Environment.TickCount与DateTime.Now的区别

73

使用Environment.TickCount计算时间跨度是否可行?

int start = Environment.TickCount;
// Do stuff
int duration = Environment.TickCount - start;
Console.WriteLine("That took " + duration " ms");

由于TickCount是带符号的,在25天后会翻转(它需要50天才能命中所有32位,但如果您想使任何数学有意义,必须抛弃符号位),因此它似乎太冒险而无法实用。

我改用DateTime.Now。这是最好的方法吗?

DateTime start = DateTime.Now;
// Do stuff
TimeSpan duration = DateTime.Now - start;
Console.WriteLine("That took " + duration.TotalMilliseconds + " ms");

16
顺便提一下,如果您要使用DateTime进行日期相关的数学计算,请始终使用DateTime.UtcNow而不是DateTime.Now。因为DateTime.Now会受夏令时影响...这可能导致您的计算错误一小时,甚至更糟,出现负数。 - Scott Hanselman
6
@Scott:值得一提的是,即使使用UtcNow,也存在定时NTP同步的问题:在这些更新后,系统时间变化10秒的情况并不少见(在我的电脑上)。 - vgru
数学实际上并不那么复杂... int duration = unchecked((int)((uint)Environment.TickCount - (uint)start)); ...将为您提供正确的答案,而不受溢出的影响。(除非您绝对需要它,否则最好跳过将其转换回 int 的步骤。) - AnorZaken
@AnorZaken:你实际上不需要任何类型转换,因为这个页面的其他地方已经解释过了。 - Glenn Slayden
@GlennSlayden 您是正确的 - 我的 uint 强制转换是无意义的,而且由于我又强制转换回 int,所以根本没有任何意义。很好地发现了这个问题。 (此外,在未经检查的上下文中将强制转换回 int 也是我自己的一个可疑选择。) - AnorZaken
14个回答

106

Environment.TickCount基于GetTickCount() WinAPI函数,以毫秒为单位计算。但它的实际精度约为15.6毫秒。因此,您无法测量更短的时间间隔(否则将得到0)。

注意:返回值为Int32,因此该计数器每~49.7天就会翻转。您不应使用它来测量这么长的时间间隔。

DateTime.Ticks基于GetSystemTimeAsFileTime() WinAPI函数,以100纳秒(百万分之一秒)为单位计算。DateTime.Ticks的实际精度取决于系统。在XP上,系统时钟的增量约为15.6毫秒,与Environment.TickCount相同。在Windows 7上,其精度为1毫秒(而Environemnt.TickCount的精度仍为15.6毫秒),但是如果使用省电方案(通常用于笔记本电脑),它也可能降至15.6毫秒。

Stopwatch基于QueryPerformanceCounter() WinAPI函数(但如果您的系统不支持高分辨率性能计数器,则使用DateTime.Ticks)

在使用StopWatch之前,请注意两个问题:

  • 它在多处理器系统上可能不可靠(请参见MS kb895980kb896256
  • 如果CPU频率变化,它可能不可靠(请阅读此文章

您可以使用简单的测试来评估系统的精度:

static void Main(string[] args)
{
    int xcnt = 0;
    long xdelta, xstart;
    xstart = DateTime.UtcNow.Ticks;
    do {
        xdelta = DateTime.UtcNow.Ticks - xstart;
        xcnt++;
    } while (xdelta == 0);

    Console.WriteLine("DateTime:\t{0} ms, in {1} cycles", xdelta / (10000.0), xcnt);

    int ycnt = 0, ystart;
    long ydelta;
    ystart = Environment.TickCount;
    do {
        ydelta = Environment.TickCount - ystart;
        ycnt++;
    } while (ydelta == 0);

    Console.WriteLine("Environment:\t{0} ms, in {1} cycles ", ydelta, ycnt);


    Stopwatch sw = new Stopwatch();
    int zcnt = 0;
    long zstart, zdelta;

    sw.Start();
    zstart = sw.ElapsedTicks; // This minimizes the difference (opposed to just using 0)
    do {
        zdelta = sw.ElapsedTicks - zstart;
        zcnt++;
    } while (zdelta == 0);
    sw.Stop();

    Console.WriteLine("StopWatch:\t{0} ms, in {1} cycles", (zdelta * 1000.0) / Stopwatch.Frequency, zcnt);
    Console.ReadKey();
}

只是提醒一下关于你的循环 - 你应该使用 Thread.SpinWait 而不是 for 循环,这将确保循环不会被优化掉(你在语义上指定了“浪费”循环是你想要的)。除此之外,它应该与 for 循环基本相同。 - Luaan
@Luaan:谢谢,我已经修改了示例代码以显示精度。 - mistika
1
非常详细的解释。在我的Win10桌面上,结果与您的Win7报告类似。StopWatch看起来像高精度计时器。DateTime:1,0278毫秒,在21272个周期内。环境:16毫秒,在4786363个周期内。StopWatch:0.00087671156014329毫秒,在1个周期内。 - Patrick Stalph
4
QueryPerformanceCounter()现在是可靠的:请参见https://blogs.msdn.microsoft.com/oldnewthing/20140822-01/?p=163和http://msdn.microsoft.com/en-us/library/windows/desktop/dn553408(v=vs.85).aspx:“问:QPC在多处理器系统、多核系统和具有超线程技术的系统上是否可靠?答:是的”,“问:处理器频率变化(由电源管理或Turbo Boost技术引起)是否影响QPC的准确性?答:不影响”。 - Ilya Serbis
6
您所提到的“问题”在如今基本上不再相关。它们适用于Windows 2000、Windows 2003和Windows XP。如果您正在运行这些系统,那么您面临的问题比计时器是否准确要更严重。 - Mark
显示剩余2条评论

74

1
Stopwatch类仅适用于.NET Framework 2.0及以上版本。在旧版本中,您可以使用TickCount,可能与TimeSpan结合使用,以检查是否已越过25天边界。 - Rune Grimstad
在早期版本中,您将使用标记为安全的p/invoke调用和http://msdn.microsoft.com/en-us/library/ms901807.aspx,而不是TickCount。 - Henrik
1
如果你的程序运行的硬件缺少高分辨率计时器,那么 Stopwatch 和 DateTime 一样不可靠。请参见http://connect.microsoft.com/VisualStudio/feedback/details/741848/system-diagnostics-stopwatch-elapsed-time-affected-by-user-changing-computers-date-time,以及下面 Joel 的回答。 - rkagerer
1
我找不到适用于可移植类库的计时器,因此您必须使用一些变通方法。 - Mando

30

你为什么担心溢出问题?只要你测量的时间不超过24.9天,并且计算相对持续时间,就没问题。无论系统运行了多长时间,只要你只关注你所占用的运行时间(而不是直接在开始和结束点上执行小于或大于比较),就可以了。也就是说:

 int before_rollover = Int32.MaxValue - 5;
 int after_rollover = Int32.MinValue + 7;
 int duration = after_rollover - before_rollover;
 Console.WriteLine("before_rollover: " + before_rollover.ToString());
 Console.WriteLine("after_rollover: " + after_rollover.ToString());
 Console.WriteLine("duration: " + duration.ToString());

正确地打印:

 before_rollover: 2147483642
 after_rollover: -2147483641
 duration: 13

你不必担心符号位。C#和C一样,让CPU处理这个问题。

这是我在嵌入式系统中遇到的常见情况。例如,我永远不会直接比较rollover之前和rollover之后的时间计数。我总是执行减法来找到考虑rollover的持续时间,然后基于该持续时间进行任何其他计算。


4
我的最佳建议是这个。除非你试图使用它来跟踪超过50天的时间,否则它不会“在50天后爆炸”,这似乎非常不可能。赞同包含易于测试的实际证明。StopWatch类是无用的,因为它存在多核CPU问题,而这几乎是现代计算机的通用情况。 - eselk
3
注意:此示例仅在代码构建时未检查算术溢出/下溢的情况下有效(默认情况下关闭)。如果您打开了它(项目设置> 构建> 高级...> 检查算术溢出/下溢 ),该代码将引发“System.OverflowException:算术操作导致溢出”异常。 - Ilya Serbis
@eselk 现在StopWatch是可靠的:请参见https://dev59.com/8HVC5IYBdhLWcg3wnCSA#M7GhEYcBWogLw_1b_896 - Ilya Serbis

11
这里是一个更新和刷新的摘要,列出了此主题中最有用的答案和评论以及额外的基准和变体:
首先:正如其他人在评论中指出的那样,随着“现代”Windows(Win XP ++)和.NET以及现代硬件的出现,情况已经发生了改变,没有或很少有理由不使用Stopwatch()。请参阅MSDN了解详情。引用如下:
“QPC准确性是否会受到由电源管理或Turbo Boost技术引起的处理器频率变化的影响?
不会。如果处理器有不变的TSC,那么这些变化不会影响QPC。如果处理器没有不变的TSC,则QPC将恢复到一个平台硬件计时器,该计时器不会受处理器频率变化或Turbo Boost技术的影响。

QPC在多处理器系统、多核系统和具有超线程的系统上可靠工作吗?
可以。

如何确定和验证QPC在我的计算机上是否有效?
您不需要执行此类检查。

哪些处理器具有非不变TSC?[...请继续阅读...]”

但是,如果您不需要Stopwatch()的精度或至少想要了解Stopwatch的性能(静态与基于实例),以及其他可能的变体,请继续阅读:

我接手了cskwg的基准测试,并扩展了更多变体的代码。我使用一台几年前的i7 4700 MQ和C# 7与VS 2017进行了测试(更精确地说,使用.NET 4.5.2编译,尽管有二进制字面量,它是C# 6(使用了字符串字面量和'using static')。特别是与提到的基准测试相比,Stopwatch()的性能似乎有所改善。

这是一个在循环中重复1000万次的结果示例,如常规一样,绝对值并不重要,但即使相对值在其他硬件上也可能有所不同:

32位、未经优化的发布模式:

测量结果: GetTickCount64() [ms]: 275
测量结果: Environment.TickCount [ms]: 45
测量结果: DateTime.UtcNow.Ticks [ms]: 167
测量结果: Stopwatch: .ElapsedTicks [ms]: 277
测量结果: Stopwatch: .ElapsedMilliseconds [ms]: 548
测量结果: static Stopwatch.GetTimestamp [ms]: 193
测量结果: Stopwatch+conversion to DateTime [ms]: 551
与DateTime.Now.Ticks [ms]相比较: 9010

32位,发布模式,优化:
测量结果:GetTickCount64() [毫秒]:198 测量结果:Environment.TickCount [毫秒]:39 测量结果:DateTime.UtcNow.Ticks [毫秒]:66(!) 测量结果:Stopwatch: .ElapsedTicks [毫秒]:175 测量结果:Stopwatch: .ElapsedMilliseconds [毫秒]:491 测量结果:static Stopwatch.GetTimestamp [毫秒]:175 测量结果:Stopwatch+转换为DateTime [毫秒]:510 将其与DateTime.Now.Ticks [毫秒]进行比较:8460
64位,发布模式,未优化:
测量结果:GetTickCount64() [毫秒]:205 测量结果:Environment.TickCount [毫秒]:39 测量结果:DateTime.UtcNow.Ticks [毫秒]:127 测量结果:Stopwatch: .ElapsedTicks [毫秒]:209 测量结果:Stopwatch: .ElapsedMilliseconds [毫秒]:285 测量结果:static Stopwatch.GetTimestamp [毫秒]:187 测量结果:Stopwatch+conversion to DateTime [毫秒]:319 与 DateTime.Now.Ticks [毫秒] 比较:3040
64 位,Release 模式,已优化:
测量结果如下:
GetTickCount64() [ms]:148
Environment.TickCount [ms]:31(还值得吗?)
DateTime.UtcNow.Ticks [ms]:76(!)
Stopwatch: .ElapsedTicks [ms]:178
Stopwatch: .ElapsedMilliseconds [ms]:226
static Stopwatch.GetTimestamp [ms]:175
Stopwatch+转换为 DateTime [ms]:246
与此相比,DateTime.Now.Ticks [ms] 为 3020。
有趣的是,创建一个 DateTime 值以打印出 Stopwatch 时间似乎几乎没有成本。更有学术意义而非实际意义的是,静态 Stopwatch 稍微快一些(正如预期)。一些优化点也很有趣。例如,我无法解释为什么 Stopwatch.ElapsedMilliseconds 只有 32 位时与其其他变体相比速度如此缓慢,例如静态变量。这样做并且使用 64 位可以使它们的速度增加一倍以上。
你可以看到:只有执行数达到百万级别,Stopwatch 的时间才开始变得重要。如果真是这样的话(但要小心过早进行微优化),使用 GetTickCount64(),尤其是 DateTime.UtcNow,可能会很有意思。它们提供了一个 64 位(long)计时器,比 Stopwatch 更快,精度较低,因此您不必处理 32 位“丑陋”的 Environment.TickCount。
如预期的那样,DateTime.Now 是最慢的。
如果你运行它,代码还会检索你当前的 Stopwatch 精度等信息。
以下是完整的基准测试代码:
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading;
using static System.Environment;

[...]

    [DllImport("kernel32.dll") ]
    public static extern UInt64 GetTickCount64(); // Retrieves a 64bit value containing ticks since system start

    static void Main(string[] args)
    {
        const int max = 10_000_000;
        const int n = 3;
        Stopwatch sw;

        // Following Process&Thread lines according to tips by Thomas Maierhofer: https://codeproject.com/KB/testing/stopwatch-measure-precise.aspx
        // But this somewhat contradicts to assertions by MS in: https://msdn.microsoft.com/en-us/library/windows/desktop/dn553408%28v=vs.85%29.aspx?f=255&MSPPError=-2147217396#Does_QPC_reliably_work_on_multi-processor_systems__multi-core_system__and_________systems_with_hyper-threading
        Process.GetCurrentProcess().ProcessorAffinity = new IntPtr(1); // Use only the first core
        Process.GetCurrentProcess().PriorityClass = ProcessPriorityClass.High;
        Thread.CurrentThread.Priority = ThreadPriority.Highest;
        Thread.Sleep(2); // warmup

        Console.WriteLine($"Repeating measurement {n} times in loop of {max:N0}:{NewLine}");
        for (int j = 0; j < n; j++)
        {
            sw = new Stopwatch();
            sw.Start();
            for (int i = 0; i < max; i++)
            {
                var tickCount = GetTickCount64();
            }
            sw.Stop();
            Console.WriteLine($"Measured: GetTickCount64() [ms]: {sw.ElapsedMilliseconds}");
            //
            //
            sw = new Stopwatch();
            sw.Start();
            for (int i = 0; i < max; i++)
            {
                var tickCount = Environment.TickCount; // only int capacity, enough for a bit more than 24 days
            }
            sw.Stop();
            Console.WriteLine($"Measured: Environment.TickCount [ms]: {sw.ElapsedMilliseconds}");
            //
            //
            sw = new Stopwatch();
            sw.Start();
            for (int i = 0; i < max; i++)
            {
                var a = DateTime.UtcNow.Ticks;
            }
            sw.Stop();
            Console.WriteLine($"Measured: DateTime.UtcNow.Ticks [ms]: {sw.ElapsedMilliseconds}");
            //
            //
            sw = new Stopwatch();
            sw.Start();
            for (int i = 0; i < max; i++)
            {
                var a = sw.ElapsedMilliseconds;
            }
            sw.Stop();
            Console.WriteLine($"Measured: Stopwatch: .ElapsedMilliseconds [ms]: {sw.ElapsedMilliseconds}");
            //
            //
            sw = new Stopwatch();
            sw.Start();
            for (int i = 0; i < max; i++)
            {
                var a = Stopwatch.GetTimestamp();
            }
            sw.Stop();
            Console.WriteLine($"Measured: static Stopwatch.GetTimestamp [ms]: {sw.ElapsedMilliseconds}");
            //
            //
            DateTime dt=DateTime.MinValue; // just init
            sw = new Stopwatch();
            sw.Start();
            for (int i = 0; i < max; i++)
            {
                var a = new DateTime(sw.Elapsed.Ticks); // using variable dt here seems to make nearly no difference
            }
            sw.Stop();
            //Console.WriteLine($"Measured: Stopwatch+conversion to DateTime [s] with millisecs: {dt:s.fff}");
            Console.WriteLine($"Measured: Stopwatch+conversion to DateTime [ms]:  {sw.ElapsedMilliseconds}");

            Console.WriteLine();
        }
        //
        //
        sw = new Stopwatch();
        var tickCounterStart = Environment.TickCount;
        sw.Start();
        for (int i = 0; i < max/10; i++)
        {
            var a = DateTime.Now.Ticks;
        }
        sw.Stop();
        var tickCounter = Environment.TickCount - tickCounterStart;
        Console.WriteLine($"Compare that with DateTime.Now.Ticks [ms]: {sw.ElapsedMilliseconds*10}");

        Console.WriteLine($"{NewLine}General Stopwatch information:");
        if (Stopwatch.IsHighResolution)
            Console.WriteLine("- Using high-resolution performance counter for Stopwatch class.");
        else
            Console.WriteLine("- Using high-resolution performance counter for Stopwatch class.");

        double freq = (double)Stopwatch.Frequency;
        double ticksPerMicroSec = freq / (1000d*1000d) ; // microsecond resolution: 1 million ticks per sec
        Console.WriteLine($"- Stopwatch accuracy- ticks per microsecond (1000 ms): {ticksPerMicroSec:N1}");
        Console.WriteLine(" (Max. tick resolution normally is 100 nanoseconds, this is 10 ticks/microsecond.)");

        DateTime maxTimeForTickCountInteger= new DateTime(Int32.MaxValue*10_000L);  // tickCount means millisec -> there are 10.000 milliseconds in 100 nanoseconds, which is the tick resolution in .NET, e.g. used for TimeSpan
        Console.WriteLine($"- Approximated capacity (maxtime) of TickCount [dd:hh:mm:ss] {maxTimeForTickCountInteger:dd:HH:mm:ss}");
        // this conversion from seems not really accurate, it will be between 24-25 days.
        Console.WriteLine($"{NewLine}Done.");

        while (Console.KeyAvailable)
            Console.ReadKey(false);
        Console.ReadKey();
    }

11

Environment.TickCount似乎比其他解决方案快得多:

Environment.TickCount 71
DateTime.UtcNow.Ticks 213
sw.ElapsedMilliseconds 1273

这些测量数据是由以下代码生成的:

static void Main( string[] args ) {
    const int max = 10000000;
    //
    //
    for ( int j = 0; j < 3; j++ ) {
        var sw = new Stopwatch();
        sw.Start();
        for ( int i = 0; i < max; i++ ) {
            var a = Environment.TickCount;
        }
        sw.Stop();
        Console.WriteLine( $"Environment.TickCount {sw.ElapsedMilliseconds}" );
        //
        //
        sw = new Stopwatch();
        sw.Start();
        for ( int i = 0; i < max; i++ ) {
            var a = DateTime.UtcNow.Ticks;
        }
        sw.Stop();
        Console.WriteLine( $"DateTime.UtcNow.Ticks {sw.ElapsedMilliseconds}" );
        //
        //
        sw = new Stopwatch();
        sw.Start();
        for ( int i = 0; i < max; i++ ) {
            var a = sw.ElapsedMilliseconds;
        }
        sw.Stop();
        Console.WriteLine( $"sw.ElapsedMilliseconds {sw.ElapsedMilliseconds}" );
    }
    Console.WriteLine( "Done" );
    Console.ReadKey();
}

2
+1 给我,因为这正是我想找到的。如果有人在阅读这篇文章:为什么这个答案没有评论,也没有其他的赞/踩?(截至2017年5月)如果有不正确的地方,请评论,如果您认为这是正确的,请点赞。 - Philm
1
我已经扩展了基准测试,请查看我的答案。顺便说一句,在发布模式或调试模式下执行这些基准测试会有有趣的差异(后者您可能不应该用于“真实”结果,但是看到哪些测试得到编译器的真正优化是很有趣的)。 - Philm

10

9
如果您需要类似于Environment.TickCount的功能,但不想创建新的Stopwatch对象,则可以使用静态方法Stopwatch.GetTimestamp()(以及Stopwatch.Frequency)来计算长时间间隔。因为GetTimestamp()返回一个long类型,所以它不会在很长一段时间内(在我编写本文时的计算机上超过100,000年)发生溢出。 它比Environment.TickCount更准确,后者的最大分辨率为10至16毫秒。

不幸的是,Stopwatch.GetTimestamp() 不像 Environment.TickCount 那样方便,因为它返回一些抽象的滴答声,而不是普通的时间单位。 - Ilya Serbis
如果有高性能的考虑,应该注意到 Environment.TickCount 明显更快。这里的基准测试 (https://blog.tedd.no/2015/08/17/comparison-of-environment-tickcount-datetime-now-ticks-and-stopwatch-elapsedmilliseconds/) 显示以下相对时间:Environment.TickCount: 7ms,DateTime.Now.Ticks: 1392ms,Stopwatch.ElapsedMilliseconds: 933ms - Special Sauce
@SpecialSauce 但请注意它们并不是等价的事物。同时请注意,在我的系统上,Stopwatch.ElapsedMilliseconds 比他的系统快约35倍,而我的 Environment.TickCount 只快约2倍。你的硬件可能会有所不同。 - Mark

8
使用
System.Diagnostics.Stopwatch

它有一个属性叫做:
EllapsedMilliseconds

3

TickCount64

对于这个新函数进行了一些快速的测量,我发现(优化、发布 64 位、1000mio 循环):

Environment.TickCount: 2265
Environment.TickCount64: 2531
DateTime.UtcNow.Ticks: 69016

未优化代码的测量结果类似。 测试代码:

static void Main( string[] args ) {
    long start, end, length = 1000 * 1000 * 1000;
    start = Environment.TickCount64;
    for ( int i = 0; i < length; i++ ) {
        var a = Environment.TickCount;
    }
    end = Environment.TickCount64;
    Console.WriteLine( "Environment.TickCount: {0}", end - start );
    start = Environment.TickCount64;
    for ( int i = 0; i < length; i++ ) {
        var a = Environment.TickCount64;
    }
    end = Environment.TickCount64;
    Console.WriteLine( "Environment.TickCount64: {0}", end - start );
    start = Environment.TickCount64;
    for ( int i = 0; i < length; i++ ) {
        var a = DateTime.UtcNow.Ticks;
    }
    end = Environment.TickCount64;
    Console.WriteLine( "DateTime.UtcNow.Ticks: {0}", end - start );
}

0

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接