【问题标题】:Why are DateTime.Now DateTime.UtcNow so slow/expensive为什么 DateTime.Now DateTime.UtcNow 如此缓慢/昂贵
【发布时间】:2011-05-03 19:06:25
【问题描述】:

我意识到这对微优化领域来说太过分了,但我很好奇为什么对 DateTime.Now 和 DateTime.UtcNow 的调用如此“昂贵”。我有一个示例程序,它运行几个场景来做一些“工作”(添加到计数器)并尝试执行此操作 1 秒钟。我有几种方法可以让它在有限的时间内完成这项工作。示例表明 DateTime.Now 和 DateTime.UtcNow 比 Environment.TickCount 慢得多,但与仅让单独的线程休眠 1 秒然后设置一个值来指示工作线程停止相比,这还是很慢。

所以我的问题是:

  • 我知道 UtcNow 更快是因为它没有时区信息,为什么它仍然比 TickCount 慢很多?
  • 为什么读取布尔值比读取 int 更快?
  • 处理这些类型的场景的理想方法是什么,您需要让某事在有限的时间内运行,但您又不想浪费更多时间检查时间而不是实际工作?李>

请原谅例子的冗长:

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;
    }

}

【问题讨论】:

    标签: c# performance micro-optimization


    【解决方案1】:

    TickCount 只是读取一个不断增加的计数器。这只是你能做的最简单的事情。

    DateTime.UtcNow 需要查询系统时间 - 不要忘记虽然 TickCount 对用户更改时钟或 NTP 之类的事情一无所知,但 UtcNow 必须考虑到这一点。

    现在您已经表达了性能问题 - 但在您给出的示例中,您所做的只是增加一个计数器。我希望在你的 real 代码中,你会做更多的工作。如果您正在进行大量的工作,这可能会使UtcNow 所花费的时间相形见绌。在做任何其他事情之前,你应该测量它,以确定你是否真的在尝试解决一个不存在的问题。

    如果您确实需要改进,那么:

    • 您可以使用计时器而不是显式创建新线程。框架中有多种计时器,在不了解您的具体情况的情况下,我无法建议使用哪种最合理 - 但感觉比启动线程更好。
    • 您可以测量任务的几次迭代,然后猜测实际需要多少次。然后,您可能希望执行一半的迭代,评估所花费的时间,然后相应地调整剩余周期数。当然,如果每次迭代所花费的时间变化很大,这将不起作用。

    【讨论】:

    • 谢谢乔恩。我会研究计时器。我意识到我的例子中的“工作”是不现实的。然而,我很好奇巨大的影响来自哪里。
    • @My Other Me:基本上,与递增计数器的工作相比,几乎任何工作都会产生很大的影响:)
    【解决方案2】:

    FWIW 这里是 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)。

    【讨论】:

    • 根据我的测试,提供的函数是简单查询 DateTime.UtcNow 的 3.5 倍
    • 更好的方法是只获得一次时间;然后启动一个计时器。每次您想知道时间时,只需将当前计时器添加到旧时间戳即可。
    • @ArsenZahray 是因为使用了很慢的DateTime.Now,用DateTime.UtcNow 在代码中更改它并再次测试。 - 可以对线程安全和不太准确的返回进行更多改进,使其更快
    【解决方案3】:

    据我所知,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));
        }
    }
    

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

    【讨论】:

      【解决方案4】:

      有关DateTime.UtcNow / DateTimeOffset.UtcNow 的分析速度的最新信息,请参阅此dotnet thread,其中BenchmarkDotNet 用于分析。

      不幸的是,与 2.2 相比,跳到 .NET (Core) 3 时出现了性能回归,但即使使用回归值报告,DateTime.UtcNow 也以 71 ns 的惊人时间出现(它已经25 ns),即十亿分之一秒。

      从这个角度来看,即使71ns 的速度较慢,这意味着:

      您可以调用DateTime.UtcNow ~ 14,000 次,只需 1 毫秒!

      在以前更快的时间 25 ns(希望他们能恢复这种性能),您可以调用 DateTime.UtcNow ~ 40,000 次,花费 1 毫秒。

      我不是在这里查看旧的 .NET Framework 时代,但至少对于较新的位,我认为可以肯定地说,至少不再准确地说 DateTime.UtcNow 是“慢/昂贵” "(我很欣赏这个问题被问到了!)。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2015-07-12
        • 1970-01-01
        • 2019-11-06
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2010-12-23
        • 2017-06-04
        相关资源
        最近更新 更多