65.9K
CodeProject 正在变化。 阅读更多。
Home

基于 DateTime 的 TimeSpan 计算是性能瓶颈

2014年7月25日

公共领域

3分钟阅读

viewsIcon

15000

基于 DateTime 的 TimeSpan 计算是性能瓶颈

DateTime.Now 这样看似微不足道的操作也可能成为瓶颈。在典型的 Windows 系统中,Environment.TickCount 的速度至少快 100 倍。你不相信吗?自己试试!这是测试代码

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace TimerPerformance
{
    using System.Diagnostics;

    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Performance Tests");
            Console.WriteLine("  Stopwatch Resolution (nS): " + 
                             (1000000000.0 / Stopwatch.Frequency).ToString());

            RunTests();

            Console.WriteLine("Tests Finished, press any key to stop...");
            Console.ReadKey();
        }

        public static long DummyValue;

        public static void RunTests()
        {
            const int loopEnd = 1000000;
            Stopwatch watch = new Stopwatch();

            Console.WriteLine();
            Console.WriteLine("Reference Loop (NOP) Iterations: " + loopEnd);
            watch.Reset();
            watch.Start();
            for (int i = 0; i < loopEnd; ++i)
            {
                DummyValue += i;
            }
            watch.Stop();
            Console.WriteLine("  Reference Loop (NOP) Elapsed Time (ms): " + 
                        ((double)watch.ElapsedTicks / Stopwatch.Frequency * 1000).ToString());


            Console.WriteLine();
            Console.WriteLine("Query Environment.TickCount");
            watch.Reset();
            watch.Start();
            for (int i = 0; i < loopEnd; ++i)
            {
                DummyValue += Environment.TickCount;
            }
            watch.Stop();
            Console.WriteLine("  Query Environment.TickCount Elapsed Time (ms): " + 
                           ((double)watch.ElapsedTicks / Stopwatch.Frequency * 1000).ToString());

            Console.WriteLine();
            Console.WriteLine("Query DateTime.Now.Ticks");
            watch.Reset();
            watch.Start();
            for (int i = 0; i < loopEnd; ++i)
            {
                DummyValue += DateTime.Now.Ticks;
            }
            watch.Stop();
            Console.WriteLine("  Query DateTime.Now.Ticks Elapsed Time (ms): " + 
                      ((double)watch.ElapsedTicks / Stopwatch.Frequency * 1000).ToString());

            Console.WriteLine();
            Console.WriteLine("Query Stopwatch.ElapsedTicks");
            watch.Reset();
            watch.Start();
            for (int i = 0; i < loopEnd; ++i)
            {
                DummyValue += watch.ElapsedTicks;
            }
            watch.Stop();
            Console.WriteLine("  Query Stopwatch.ElapsedTicks Elapsed Time (ms): " + 
                  ((double)watch.ElapsedTicks / Stopwatch.Frequency * 1000).ToString());
        }        
    }
}

以下是一些机器的测试结果(1.000.000 次迭代,单位:毫秒)

硬件 空循环 Environment.TickCount DateTime.Now.Ticks
AMD Opteron 4174 HE 2.3 GHz 8.7 毫秒 16.6 毫秒 2227 毫秒
AMD Athlon 64 X2 5600+ 2.9 GHz 6.8 毫秒 15.1 毫秒 1265 毫秒
Intel Core 2 Quad Q9550 2.83 GHz 2.1 毫秒 4.9 毫秒 557.8 毫秒
Azure A1 (Intel Xeon E5-2660 2.2 GHz) 5.2 毫秒 19.9 毫秒 168.1 毫秒

好的,单个请求只需要大约 1-2 微秒的 DateTime.Now 调用时间。这意味着最大吞吐量为每秒 500.000 到 1.000.000 次调用。相比之下,Environment.TickCount 的最大吞吐量约为每秒 600.000.000 次调用。如果某个特定操作需要 10 个时间戳,那么由于 DateTime.Now,其最大吞吐量仅为 50.000 个操作。例如,测量响应时间和吞吐量(数据传输速率)的 HTTP 请求需要为从 Web 服务器接收的每个数据块的时间戳。在操作完成之前,至少有 3 个时间戳(开始、响应、结束)用于测量响应时间和下载时间。如果测量吞吐量(数据传输速率),则完全取决于接收到的数据块数量。这对于多线程访问来说更是个问题。Environment.TickCountDateTime.Now 都是共享资源。所有调用都必须经过它们的同步机制,这意味着它们不能并行化。

Crawler-Lib Engine 这样的实际系统可以在相对较好的硬件上每秒执行 20.000 - 30.000 个 HTTP 请求。因此,很明显,时间测量会对最大吞吐量产生影响。

有些人会认为,DateTime.NowEnvironment.TickCount 精确得多。这部分是正确的。这里有一个代码片段,用于测量时间戳的粒度

if( Environment.TickCount > int.MaxValue - 60000) 
throw new InvalidOperationException("Tick Count will overflow in the next minute, test can't be run");
var startTickCount = Environment.TickCount;
var currentTickCount = startTickCount;
int minGranularity = int.MaxValue;
int maxGranularity = 0;

while (currentTickCount < startTickCount + 1000)
{
    var tempMeasure = Environment.TickCount;
    if (tempMeasure - currentTickCount > 0)
    {
        minGranularity = Math.Min(minGranularity, tempMeasure - currentTickCount);
        maxGranularity = Math.Max(maxGranularity, tempMeasure - currentTickCount);
    }
    currentTickCount = tempMeasure;
    Thread.Sleep(0);
}
Console.WriteLine("Environment.TickCount Min Granularity: " + minGranularity + ", 
                   Max Granularity: " + maxGranularity + " ms");

Console.WriteLine();

var startTime = DateTime.Now;
var currentTime = startTime;
double minGranularityTime = double.MaxValue;
double maxGranularityTime = 0.0;

while (currentTime < startTime + new TimeSpan(0, 0, 1))
{
    var tempMeasure = DateTime.Now;
    if ((tempMeasure - currentTime).TotalMilliseconds > 0)
    {
        minGranularityTime = Math.Min(minGranularityTime, 
                            (tempMeasure - currentTime).TotalMilliseconds);
        maxGranularityTime = Math.Max(maxGranularityTime, 
                            (tempMeasure - currentTime).TotalMilliseconds);
    }
    currentTime = tempMeasure;
    Thread.Sleep(0);
}
Console.WriteLine("DateTime Min Granularity: " + minGranularityTime + ", 
                   Max Granularity: " + maxGranularityTime + " ms");

在多台机器上运行此代码表明,Environment.TickCount 的粒度约为 16 毫秒(15.6 毫秒),这是默认的系统范围计时器分辨率。可以使用 timeBeginPeriod 函数将系统范围计时器分辨率降低到 1 毫秒,但这通常不建议这样做,因为它会影响所有应用程序。DateTime.Now 在某些机器上的粒度为 16 毫秒,在其他机器上粒度更好,可达 1 毫秒。但它从来不会好到超过 1 毫秒。如果您需要测量更小的时间,则必须使用 System.Diagnostics.Stopwatch 类,它实际上是一个高分辨率计时器。

因此,Crawler-Lib Framework 使用 Environment.TickCount 来记录测量响应、任务或任何内容持续时间所需的时间戳。我们很快将免费发布 Crawler-Lib Core 库,其中包含一个 TickTimestamp 类,可用于持续时间和吞吐量计算。

© . All rights reserved.