为什么 DateTime.Now 和 DateTime.UtcNow 如此缓慢/昂贵

38

我知道这已经涉及到微观优化领域,但我很好奇为什么调用DateTime.Now和DateTime.UtcNow会如此"昂贵"。我有一个样本程序,运行几个场景来执行一些"工作"(增加计数器),并尝试在1秒钟内完成。我有几种方法可以让它在有限的时间内执行工作。示例表明,DateTime.Now和DateTime.UtcNow比Environment.TickCount慢得多,但即使与让单独的线程休眠1秒钟然后设置值以指示工作线程停止相比,后者也很慢。

我的问题是:

  • 我知道UtcNow更快,因为它没有时区信息,为什么它仍然比TickCount慢那么多?
  • 为什么读取布尔型变量比读取整型变量更快?
  • 处理这些类型的场景的理想方式是什么?你需要允许某些东西运行有限的时间,但你不想浪费更多的时间来检查时间而不是真正地做事情?

请原谅示例的冗长:

class Program
{
    private static volatile bool done = false;
    private static volatile int doneInt = 0;
    private static UInt64 doneLong = 0;

    private static ManualResetEvent readyEvent = new ManualResetEvent(false);

    static void Main(string[] args)
    {
        MethodA_PrecalcEndTime();
        MethodB_CalcEndTimeEachTime();
        MethodC_PrecalcEndTimeUsingUtcNow();

        MethodD_EnvironmentTickCount();

        MethodX_SeperateThreadBool();
        MethodY_SeperateThreadInt();
        MethodZ_SeperateThreadLong();

        Console.WriteLine("Done...");
        Console.ReadLine();
    }

    private static void MethodA_PrecalcEndTime()
    {
        int cnt = 0;
        var doneTime = DateTime.Now.AddSeconds(1);
        var startDT = DateTime.Now;
        while (DateTime.Now <= doneTime)
        {
            cnt++;
        }
        var endDT = DateTime.Now;
        Console.WriteLine("Time Taken: {0,30} Total Counted: {1,20}", endDT.Subtract(startDT), cnt);
    }

    private static void MethodB_CalcEndTimeEachTime()
    {
        int cnt = 0;
        var startDT = DateTime.Now;
        while (DateTime.Now <= startDT.AddSeconds(1))
        {
            cnt++;
        }
        var endDT = DateTime.Now;
        Console.WriteLine("Time Taken: {0,30} Total Counted: {1,20}", endDT.Subtract(startDT), cnt);
    }

    private static void MethodC_PrecalcEndTimeUsingUtcNow()
    {
        int cnt = 0;
        var doneTime = DateTime.UtcNow.AddSeconds(1);
        var startDT = DateTime.Now;
        while (DateTime.UtcNow <= doneTime)
        {
            cnt++;
        }
        var endDT = DateTime.Now;
        Console.WriteLine("Time Taken: {0,30} Total Counted: {1,20}", endDT.Subtract(startDT), cnt);
    }


    private static void MethodD_EnvironmentTickCount()
    {
        int cnt = 0;
        int doneTick = Environment.TickCount + 1000; // <-- should be sane near where the counter clocks...
        var startDT = DateTime.Now;
        while (Environment.TickCount <= doneTick)
        {
            cnt++;
        }
        var endDT = DateTime.Now;
        Console.WriteLine("Time Taken: {0,30} Total Counted: {1,20}", endDT.Subtract(startDT), cnt);
    }

    private static void MethodX_SeperateThreadBool()
    {
        readyEvent.Reset();
        Thread counter = new Thread(CountBool);
        Thread waiter = new Thread(WaitBool);
        counter.Start();
        waiter.Start();
        waiter.Join();
        counter.Join();
    }

    private static void CountBool()
    {
        int cnt = 0;
        readyEvent.WaitOne();
        var startDT = DateTime.Now;
        while (!done)
        {
            cnt++;
        }
        var endDT = DateTime.Now;
        Console.WriteLine("Time Taken: {0,30} Total Counted: {1,20}", endDT.Subtract(startDT), cnt);
    }

    private static void WaitBool()
    {
        readyEvent.Set();
        Thread.Sleep(TimeSpan.FromSeconds(1));
        done = true;
    }

    private static void MethodY_SeperateThreadInt()
    {
        readyEvent.Reset();
        Thread counter = new Thread(CountInt);
        Thread waiter = new Thread(WaitInt);
        counter.Start();
        waiter.Start();
        waiter.Join();
        counter.Join();
    }

    private static void CountInt()
    {
        int cnt = 0;
        readyEvent.WaitOne();
        var startDT = DateTime.Now;
        while (doneInt<1)
        {
            cnt++;
        }
        var endDT = DateTime.Now;
        Console.WriteLine("Time Taken: {0,30} Total Counted: {1,20}", endDT.Subtract(startDT), cnt);
    }

    private static void WaitInt()
    {
        readyEvent.Set();
        Thread.Sleep(TimeSpan.FromSeconds(1));
        doneInt = 1;
    }

    private static void MethodZ_SeperateThreadLong()
    {
        readyEvent.Reset();
        Thread counter = new Thread(CountLong);
        Thread waiter = new Thread(WaitLong);
        counter.Start();
        waiter.Start();
        waiter.Join();
        counter.Join();
    }

    private static void CountLong()
    {
        int cnt = 0;
        readyEvent.WaitOne();
        var startDT = DateTime.Now;
        while (doneLong < 1)
        {
            cnt++;
        }
        var endDT = DateTime.Now;
        Console.WriteLine("Time Taken: {0,30} Total Counted: {1,20}", endDT.Subtract(startDT), cnt);
    }

    private static void WaitLong()
    {
        readyEvent.Set();
        Thread.Sleep(TimeSpan.FromSeconds(1));
        doneLong = 1;
    }

}
4个回答

35

TickCount 仅仅读取一个不断增长的计数器,这是你可以实现的最简单的事情。

DateTime.UtcNow 需要查询系统时间,而且不要忘记,TickCount 并不关心诸如用户更改时钟或NTP等问题,但 UtcNow 需要考虑到这些因素。

现在你表达了性能上的担忧,但在你给出的示例中,你只是在增加一个计数器。我预计在你的 实际 代码中,你将会做比这更多的工作。如果你正在做一些 重要的 工作,那么它可能会使 UtcNow 花费的时间微不足道。在任何其他操作之前,你应该测量一下时间以确定你是否真的在解决不存在的问题。

如果你确实需要提高性能,则可以:

  • 使用定时器而不是显式地创建一个新线程。框架中有各种类型的计时器,我无法根据你的具体情况提供建议,但这似乎比启动线程更好的解决方案。
  • 你可以测量几次迭代,然后猜测实际上需要多少次。然后你可能想要执行一半的迭代次数,记录花费的时间,然后相应地调整剩余周期的数量。当然,如果每次迭代所需的时间变化很大,这种方法就不起作用。

谢谢Jon。我会研究计时器。我意识到我的示例中的“工作”是不现实的。然而,我很好奇影响巨大的原因是什么。 - My Other Me
1
@My Other Me:基本上,与递增计数器的工作相比,几乎任何工作都会被视为巨大的影响 :) - Jon Skeet

23

这是NLog用来获取每个日志消息时间戳的一些代码。在这种情况下,“工作”的实际内容是检索当前时间(尽管它发生在可能更昂贵的“工作”即消息记录的上下文中)。 NLog通过仅在当前滴答计数与上一个滴答计数不同时获取“真实”时间(通过DateTime.Now)来最小化获取当前时间的成本。虽然这不直接适用于您的问题,但这是一种有趣的提高当前时间检索速度的方法。

internal class CurrentTimeGetter    
{        
  private static int lastTicks = -1;        
  private static DateTime lastDateTime = DateTime.MinValue;        

  /// <summary>        
  /// Gets the current time in an optimized fashion.        
  /// </summary>        
  /// <value>Current time.</value>        

  public static DateTime Now        
  {            
    get            
    {                
      int tickCount = Environment.TickCount;                
      if (tickCount == lastTicks)                
      {                    
        return lastDateTime;                
      }                
      DateTime dt = DateTime.Now;                
      lastTicks = tickCount;                
      lastDateTime = dt;                
      return dt;            
    }        
  }    
}

// It would be used like this:
DateTime timeToLog = CurrentTimeGetter.Now;
在您的问题的背景下,您可以通过以下方式“改进”时间循环代码的性能:
private static void MethodA_PrecalcEndTime()
{
  int cnt = 0;
  var doneTime = DateTime.Now.AddSeconds(1);
  var startDT = CurrentTimeGetter.Now;
  while (CurrentTimeGetter.Now <= doneTime)                            
  {           
    cnt++;
  }
  var endDT = DateTime.Now;
  Console.WriteLine("Time Taken: {0,30} Total Counted: {1,20}", endDT.Subtract(startDT), cnt);                        }                             
}
如果频繁调用CurrentTimeGetter.Now会导致返回的时间多次连续相同,那么只需要支付Environment.TickCount的成本。我不能确定它是否真的有助于提高NLog日志记录的性能,以至于你是否能够注意到。我不知道它是否对你的问题有所帮助,或者你是否还需要任何帮助,但我认为这将作为一个有趣的例子来利用更快的操作(Environment.Ticks)在某些情况下潜在地加快一个相对较慢的操作(DateTime.Now)。

1
根据我的测试,提供的函数比简单查询DateTime.UtcNow慢3.5倍。 - Arsen Zahray
1
一个更好的方法是仅在开始时获取时间,然后启动计时器。每次想要知道时间时,只需将当前计时器添加到旧的时间戳即可。 - Riki
1
@ArsenZahray,之所以缓慢是因为使用了DateTime.Now,请在代码中更改为DateTime.UtcNow并重新测试。可以进行更多的改进以实现线程安全和更少精度的返回,从而使其运行更快。 - Aristos

14
为了了解DateTime.UtcNow/DateTimeOffset.UtcNow的性能,请参阅此dotnet线程,其中使用BenchmarkDotNet进行了分析。
不幸的是,在从.NET(Core)2.2升级到3后,性能出现了退化,但即使在退化值下,DateTime.UtcNow仍然可达到非常快的71ns(原本是25 ns),即71亿分之一秒。
为了更好地理解,即使速度较慢的71ns,也意味着:
只需1毫秒的代价,您可以调用DateTime.UtcNow大约14,000次!
以前更快的速度25 ns(希望他们会恢复这种性能),您可以调用DateTime.UtcNow大约40,000次,只需要1毫秒的代价。
我没有考虑旧的.NET Framework时间,但至少对于新版本,我认为可以肯定地说DateTime.UtcNow不再被认为是“慢 / 昂贵”的(尽管很感谢提出这个问题!)。

6
据我所知,DateTime.UtcNow(不要与速度较慢的DateTime.Now混淆)是获取时间最快的方法。 事实上,像@wageoghe建议的那样对其进行缓存会显著降低性能(在我的测试中,降低了3.5倍)。
在ILSpy中,UtcNow的代码如下:
[__DynamicallyInvokable]
public static DateTime UtcNow
{
    [__DynamicallyInvokable, TargetedPatchingOptOut("Performance critical to inline across NGen image boundaries"), SecuritySafeCritical]
    get
    {
        long systemTimeAsFileTime = DateTime.GetSystemTimeAsFileTime();
        return new DateTime((ulong)(systemTimeAsFileTime + 504911232000000000L | 4611686018427387904L));
    }
}

我认为这表明编译器将该函数内联以实现最大速度。可能有更快的获取时间的方法,但到目前为止,我还没有看到过。


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