Почему DateTime.Now DateTime.UtcNow такой медленный/дорогой

Я понимаю, что это слишком далеко в области микрооптимизации, но мне любопытно понять, почему вызовы 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;
    }

}

person My Other Me    schedule 02.11.2010    source источник


Ответы (4)


TickCount просто считывает постоянно увеличивающийся счетчик. Это просто самое простое, что вы можете сделать.

DateTime.UtcNow нужно запрашивать системное время — и не забывайте, что, хотя TickCount в блаженном неведении о таких вещах, как изменение пользователем часов или NTP, UtcNow должен это учитывать.

Теперь вы выразили беспокойство по поводу производительности, но в приведенных вами примерах все, что вы делаете, это увеличиваете счетчик. Я ожидаю, что в вашем реальном коде вы будете выполнять гораздо больше работы. Если вы выполняете значительный объем работы, это, вероятно, превысит время, затрачиваемое UtcNow. Прежде чем делать что-либо еще, вы должны измерить это, чтобы выяснить, действительно ли вы пытаетесь решить проблему, которой не существует.

Если вам нужно что-то улучшить, то:

  • Вы можете использовать таймер вместо явного создания нового потока. В фреймворке есть различные виды таймеров, и, не зная вашей точной ситуации, я не могу посоветовать, какой из них будет наиболее разумным использовать, но это кажется лучшим решением, чем запуск потока.
  • Вы можете измерить несколько итераций своей задачи, а затем угадать, сколько на самом деле потребуется. Затем вы можете выполнить половину этого количества итераций, оценить, сколько времени это заняло, а затем соответствующим образом скорректировать количество оставшихся циклов. Конечно, это не работает, если время, необходимое для каждой итерации, может сильно различаться.
person Jon Skeet    schedule 02.11.2010
comment
Спасибо, Джон. Буду смотреть таймеры. Я понимаю, что работа в моем примере нереалистична. Однако мне было любопытно, откуда исходит такое большое влияние. - person My Other Me; 02.11.2010
comment
@My Other Me: По сути, по сравнению с работой по увеличению счетчика, почти любая работа будет иметь большое значение :) - person Jon Skeet; 02.11.2010

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) при некоторых обстоятельствах.

person wageoghe    schedule 06.01.2011
comment
согласно моим тестам, предоставленная функция в 3,5 раза медленнее, чем простой запрос DateTime.UtcNow - person Arsen Zahray; 29.11.2015
comment
лучшим подходом было бы получить время только один раз; затем запустите таймер. Каждый раз, когда вы хотите узнать время, вы просто добавляете текущий таймер к старой метке времени. - person Riki; 08.11.2018
comment
@ArsenZahray, потому что он использует медленный DateTime.Now, измените его в коде на DateTime.UtcNow и снова проверьте. - Может потребоваться еще больше улучшений для поточно-ориентированного и менее точного возврата, чтобы сделать его еще быстрее. - person Aristos; 16.09.2020

Актуальную информацию о профилированной скорости DateTime.UtcNow / DateTimeOffset.UtcNow см. в этой потоке dotnet, где BenchmarkDotNet использовался для профилирования.

К сожалению, при переходе на .NET (Core) 3 произошла регрессия производительности по сравнению с 2.2, но даже отчет с регрессивным значением, DateTime.UtcNow приходит в довольно качающееся время 71 ns (было 25 ns), т.е. Второй.

Для сравнения, даже при меньшей скорости 71ns это означает:

Вы можете позвонить DateTime.UtcNow ~ 14 000 раз всего за 1 миллисекунду!

В ранее более быстром времени 25 ns (надеемся, они вернут эту производительность), вы можете вызывать DateTime.UtcNow ~ 40 000 раз за 1 миллисекунду.

Я не смотрю здесь на старые времена .NET Framework, но, по крайней мере, с более новыми битами, я думаю, можно с уверенностью заявить, что, по крайней мере, уже не точно говорить, что DateTime.UtcNow является «медленным/дорогим» (я ценю это вопрос был задан однако!).

person Nicholas Petersen    schedule 13.08.2019

Насколько я могу судить, 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));
    }
}

Я думаю, это говорит о том, что функция встроена компилятором для достижения максимальной скорости. Возможно, есть более быстрые способы получить время, но пока я не видел ни одного.

person Arsen Zahray    schedule 29.11.2015