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





0/5 (0投票)
基于 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.TickCount
和 DateTime.Now
都是共享资源。所有调用都必须经过它们的同步机制,这意味着它们不能并行化。
像 Crawler-Lib Engine 这样的实际系统可以在相对较好的硬件上每秒执行 20.000 - 30.000 个 HTTP 请求。因此,很明显,时间测量会对最大吞吐量产生影响。
有些人会认为,DateTime.Now
比 Environment.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
类,可用于持续时间和吞吐量计算。