Как стать автором
Обновить
757.27
OTUS
Цифровые навыки от ведущих экспертов

Темная сторона DateTime.Now

Время на прочтение8 мин
Количество просмотров17K
Автор оригинала: Keyvan Nayyeri

Перевод статьи, опубликованной в 2011 г.

DateTime.Now — одно из наиболее часто используемых свойств в .NET Framework. Несмотря на то, что это свойство предназначено для определенных целей, из-за недостатка понимания и сноровки многие .NET-разработчики используют его при неправильных обстоятельствах, когда следует использовать другие доступные (и рекомендованные) варианты, такие как свойство DateTime.UtcNow и класс Stopwatch. В этой статье мы обсудим эти три варианта, область применения каждого из них и проведем количественное сравнение между ними, чтобы показать, почему DateTime.Now во многих случаях обходится нам слишком дорого и не должно быть использовано.

Введение

Несмотря на то, что за последнее десятилетие наблюдалось невероятное развитие .NET Framework, и было добавлено много методов и инструментов, облегчающих жизнь разработчикам программного обеспечения, все же все еще остаются некоторые недочеты в преподавании основ .NET программирования, которые сказываются на производительности .NET-приложений, написанных по всему миру и развернутых в разных масштабах.

Одной из самых распространенных ошибок у разработчиков является злоупотребление свойством DateTime.Now, предназначенного для определенных целей, но слишком часто используемого в случаях, когда рекомендовано использовать свойство DateTime.UtcNow или класс Stopwatch.

В этой статье мы рассмотрим три варианта работы с DateTime значениями в .NET Framework, изучим их внутреннюю работу, где и когда их следует использовать, а после проведем количественное сравнение между ними, чтобы понять, почему использование DateTime.Now следует ограничить.

DateTime.Now

Now — это хорошо известное и часто используемое свойство структуры DateTime из .NET, которое просто возвращает текущую дату и время на компьютере, выполняющем код. Такое свойство предоставляется почти в всеми языками программирования в качестве встроенной фичи и имеет множество применений. К сожалению, большинство .NET-разработчиков годами неправильно используют это свойство, не подозревая о его изначальном предназначении. Я могу предположить две возможные причины этой проблемы:

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

Регулярное использование .NET-разработчиками аналогичных методов в разных языках в прошлом, сформировало у них (вредную) привычку, в то время как внутренняя работа этого свойства может немного отличаться от того, что доступно в других языках.

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

Скажем так, свойство Now структуры DateTime НЕ ПРЕДНАЗНАЧЕНО для использования в случаях, когда вы хотите получить время для внутренних вычислений в вашей программе, сохранить DateTime значение в базе данных, или рассчитать производительность фрагмента кода во время выполнения. Напротив, оно ПРЕДНАЗНАЧЕНО для тех ситуаций, когда вы хотите отобразить текущую дату и время машины для ваших пользователей или сохранить какое-либо значение в локальных логах, где вы хотели бы использовать локальное время.

Трудно дать общие рекомендации относительно правильного и неправильного использования этого свойства, но я думаю, что приведенных выше примеров достаточно чтобы натолкнуть вас на мысль, когда и где использовать это свойство. Хороший вопрос, которым вы можете задаться об этом свойстве, будет: а почему мы вообще разделяем эти варианты использования и считаем некоторые из плохой практикой. Ответ кроется во внутренней реализации свойства Now, которая показана в листинге 1.

Листинг 1: Внутренняя реализация DateTime.Now

public static DateTime Now
{
      get
      {
            DateTime utcNow = DateTime.UtcNow;
            bool isAmbiguousDst = false;
            long ticks = TimeZoneInfo.GetDateTimeNowUtcOffsetFromUtc(utcNow, 
                  out isAmbiguousDst).Ticks;
            long num = utcNow.Ticks + ticks;
            if (num > 3155378975999999999L)
            {
                  return new DateTime(3155378975999999999L, DateTimeKind.Local);
            }
            if (num < 0L)
            {
                  return new DateTime(0L, DateTimeKind.Local);
            }
            return new DateTime(num, DateTimeKind.Local, isAmbiguousDst);
      }
}

По сути, это свойство преобразует текущую дату и время в формате UTC в локальные значения, что связано с дополнительной обработкой, которая является источником накладных расходов, о которых я расскажу позже.

DateTime.UtcNow

Еще одно одно очень популярное свойство DateTime — это UtcNow, хотя оно не используется так часто, как свойство Now. Это свойство возвращает текущую дату и время в формате UTC. Это свойство предназначено для использования во многих местах, где ошибочно используется Now (я перечислил некоторые из основных примеров в предыдущем разделе).

Как правило, если вы собираетесь хранить DateTime значения в базе данных или использовать их в вычислениях, вам лучше использовать UtcNow, потому что в первом случае это поможет вам получить унифицированное значение вне зависимости от локального времени компьютера, на котором вы запускаете вашу программу, а во втором случае просто нет разницу между продолжительностью времени, рассчитанной Now и UtcNow.

Как можно заметить ниже, UtcNow имеет более простую реализацию, чем Now (листинг 2). На самом деле свойство Now было написано на основе UtcNow с некоторыми дополнениями

Листинг 2: Внутренняя реализация DateTime.UtcNow

public static DateTime UtcNow
{
      [TargetedPatchingOptOut("Performance critical to inline across NGen 
            image boundaries"), SecuritySafeCritical]
      get
      {
            long systemTimeAsFileTime = DateTime.GetSystemTimeAsFileTime();
            return new DateTime((ulong)(systemTimeAsFileTime + 
                  504911232000000000L | 4611686018427387904L));
      }
}

Stopwatch

Менее популярный инструмент DateTime вычислений в .NET — это Stopwatch, класс который разработан, чтобы помочь программистам вычислять производительность фрагмента кода во время его выполнения. Большинство программистов, как правило, проставляют DateTime.Now и DateTime.UtcNow до и после фрагмента кода, чтобы определить время, необходимое для его выполнения.

Stopwatch полагается на публичный статический метод GetTimeStamp, который работает в двух режимах. Если он не используется в режиме высокого разрешения, он применяет DateTime.UtcNow, в противном случае он применяет QueryPerformanceCounter из Windows API (листинг 3), который мы обсудим в следующем разделе.

Листинг 3: Реализация GetTimeStamp Stopwatch

public static long GetTimestamp()
{
      if (Stopwatch.IsHighResolution)
      {
            long result = 0L;
            SafeNativeMethods.QueryPerformanceCounter(out result);
            return result;
      }
      return DateTime.UtcNow.Ticks;
}

QueryPerformanceCounter

То, о чем я собираюсь рассказать, вряд ли входит в перечень вещей, которые должен использовать или даже просто учитывать рядовой .NET-разработчик, тем не менее QueryPerformanceCounter — это метод Windows API, который закулисно используется некоторыми встроенными типами .NET. Это считается устаревшим подходом в .NET-разработке, ведь здесь вам необходимо использовать API интероперабельности, чтобы получить доступ к функции, которая обеспечивает доступ к счетчику производительности с высоким разрешением (листинг 4).

Листинг 4. Применение интероперабельности для использования QueryPerformanceCounter

[DllImport("Kernel32.dll")]
public static extern void QueryPerformanceCounter(ref long ticks);

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

Сравнение

Нет ничего лучше, чем видеть конкретные числа, отражающие то, о чем я говорю выше. Для этого нам понадобятся какие-нибудь простенькие программы, которые сравнивают производительность DateTime.Now, DateTime.UtcNow и Stopwatch. Обычно трудно сравнивать свойства структуры DateTime с Stopwatch, поскольку они отличаются по своей природе, однако, проявив немного смекалки при выборе примеров, можно связать их вместе и сравнить их производительность во время выполнения.

Я написал три фрагмента кода, которые достигают одной цели, используя эти три подхода. Я генерирую выборки разного размера (увеличиваясь на 500, получая 10 пакетов выборок данных) и выполняю очень простую (и бессмысленную) задачу. Я использую DateTime.Now, DateTime.UtcNow и Stopwatch, чтобы рассчитать время, необходимое для работы моего кода. Я измеряю затраченное время с помощью QueryPerformanceCounter, чтобы получить более высокую точность.

Листинг 5: Код для тестирования DateTime.Now

private static void TestDateTimeNow()
{
    Console.WriteLine("Testing DateTime.Now ...");
 
    for (int sampleSize = 500; sampleSize <= 5000; sampleSize += 500)
    {
        long start = 0;
        QueryPerformanceCounter(ref start);
 
        for (int counter = 0; counter < sampleSize; counter++)
        {
            DateTime startTime = DateTime.Now;
 
                int dumbSum = 0;
                for (int temp = 0; temp < 5; temp++)
                    dumbSum++;
 
            DateTime endTime = DateTime.Now;
 
            TimeSpan duration = endTime - startTime;
        }
 
        long end = 0;
        QueryPerformanceCounter(ref end);
            
        long time = 0;
        time = end - start;
 
        Console.WriteLine("Sample Size: {0} - Time: {1}", sampleSize, time);
    }
}

Очень похожий код можно использовать с DateTime.UtcNow (листинг 6).

Листинг 6. Код для тестирования DateTime.UtcNow

private static void TestDateTimeUtcNow()
{
    Console.WriteLine("Testing DateTime.UtcNow ...");
 
    for (int sampleSize = 500; sampleSize <= 5000; sampleSize += 500)
    {
        long start = 0;
        QueryPerformanceCounter(ref start);
 
        for (int counter = 0; counter < sampleSize; counter++)
        {
            DateTime startTime = DateTime.UtcNow;
 
                int dumbSum = 0;
                for (int temp = 0; temp < 5; temp++)
                    dumbSum++;
 
            DateTime endTime = DateTime.UtcNow;
 
            TimeSpan duration = endTime - startTime;
        }
 
        long end = 0;
        QueryPerformanceCounter(ref end);
            
        long time = 0;
        time = end - start;
 
        Console.WriteLine("Sample Size: {0} - Time: {1}", sampleSize, time);
    }
}

И, наконец, Stopwatch можно применить для измерения времени выполнения этого кода, с помощью его методов Start, Stop и свойства Elapsed (листинг 7).

Листинг 7: Код для тестирования Stopwatch

private static void TestStopwatch()
{
    Console.WriteLine("Testing Stopwatch ...");
 
    for (int sampleSize = 500; sampleSize <= 5000; sampleSize += 500)
    {
        long start = 0;
        QueryPerformanceCounter(ref start);
 
        for (int counter = 0; counter < sampleSize; counter++)
        {
            Stopwatch watch = new Stopwatch();
 
            watch.Start();
 
            int dumbSum = 0;
            for (int temp = 0; temp < 5; temp++)
                dumbSum++;
 
            watch.Stop();
 
            TimeSpan duration = watch.Elapsed;
        }
 
        long end = 0;
        QueryPerformanceCounter(ref end);
 
        long time = 0;
        time = end - start;
 
        Console.WriteLine("Sample Size: {0} - Time: {1}", sampleSize, time);
    }
}

Иногда картинка стоит тысячи слов, и таблица 1 отражает все то, о чем я говорил.

Рисунок 1: Количественное сравнение между DateTime.Now, DateTime.UtcNow и Stopwatch

Результаты, отображенные в таблице, довольно интересные, и они становятся еще интереснее, если учесть тот факт, что эти фрагменты кода тестируются на реалистичных размерах выборок данных, которые варьируются от 500 до 5000. DateTime.Now (синяя линия) здесь лидирует в топе худшей производительности, за ним следует Stopwatch (зеленая линия) и DateTime.UtcNow (красная линия). Результаты UtcNow намного ниже других подходов, и растут очень медленно (в отличие от двух других подходов) и показали себя лучше, чем Stopwatch. Это, конечно, было ожидаемо, поскольку я показал вам внутреннюю реализацию Stopwatch, которая использует DateTime.UtcNow с некоторой дополнительной обработкой.

Логичным вопросом будет: зачем нам использовать Stopwatch, если DateTime.UtcNow работает лучше. Ответ заключается в том, что возможно так и следует делать, если вы уверены, что получите такую ​​значительную разницу, применив UtcNow, а не Stopwatch. Но все же есть два преимущества Stopwatch перед UtcNow: значительным и незначительным. Значительным преимуществом является то, что Stopwatch может работать с более высоким разрешением, применяя QueryPerformanceCounter, а DateTime.UtcNow — нет. Незначительным преимуществом является то, что Stopwatch обеспечивает более быстрый и понятный для пользователя метод выполнения этой задачи.

Заключение

Многие разработчики .NET неправильно используют свойства структуры DateTime применяя свойство Now для целей, для которых оно не предназначено, что снижает производительность кода. UtcNow — это то, что следует использовать во многих случаях, вместо свойства Now. Для замеров времени существует специальный класс Stopwatch.

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


Материал подготовлен в рамках специализации "C# Developer".

Теги:
Хабы:
Всего голосов 13: ↑10 и ↓3+10
Комментарии33

Публикации

Информация

Сайт
otus.ru
Дата регистрации
Дата основания
Численность
101–200 человек
Местоположение
Россия
Представитель
OTUS