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

My *_* Me 21 c# performance micro-optimization

我意识到这在微优化领域太过分了,但我很想知道为什么调用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;
    }

}
Run Code Online (Sandbox Code Playgroud)

Jon*_*eet 22

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

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

现在你已经表达了一个性能问题 - 但是在你给出的例子中,你所做的只是增加一个计数器.我希望在你真正的代码中,你会做更多的工作.如果你正在做一个显著的工作量,这可能由相形见绌所花费的时间UtcNow.在做任何其他事情之前,你应该测量一下,看看你是否真的试图解决一个不存在的问题.

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

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


wag*_*ghe 19

FWIW这里是NLog用于获取每条日志消息的时间戳的一些代码.在这种情况下,"工作"是当前时间的实际检索(被授予,它发生在可能更昂贵的"工作"位,消息的记录的上下文中).DateTime.Now如果当前的滴答计数与先前的滴答计数不同,NLog通过仅获得"实际"时间(通过)来最小化获得当前时间的成本.这并不直接适用于您的问题,但它是一种"加速"当前时间检索的有趣方式.

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;
Run Code Online (Sandbox Code Playgroud)

在您的问题的上下文中,您可能会"改善"时间循环代码的性能,如下所示:

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);                        }                             
}
Run Code Online (Sandbox Code Playgroud)

如果CurrentTimeGetter.Now频繁调用以使返回的时间连续多次相同,则只需Environment.TickCount支付费用.我不能说它是否真的有助于NLog日志记录的性能,你会注意到或不.

我不知道这对你的问题真的有帮助,或者你是否还需要任何帮助,但我认为它可以作为一个有趣的例子,利用更快的操作(Environment.Ticks)来加速相对较慢的操作(DateTime.Now)在某些情况下.


Ars*_*ray 5

据我所知,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));
    }
}
Run Code Online (Sandbox Code Playgroud)

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


Nic*_*sen 5

有关DateTime.UtcNow/的配置文件速度的最新信息DateTimeOffset.UtcNow,请参见dotnet线程,该BenchmarkDotNet文件用于配置文件。

不幸的是,与2.2相比,跳至.NET(Core)3进行了性能回归,但是即使报告了回归值,也要花DateTime.UtcNow相当长的时间71 ns25 ns即710亿分之一秒)。

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

您只需DateTime.UtcNow花费1毫秒的时间即可拨打〜14,000次!

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

我不是在看旧的.NET Framework时代,但至少可以使用较新的版本,我认为可以肯定地说,DateTime.UtcNow“慢/昂贵” 至少不再准确了(我感谢问题被问了!)。