Перевод статьи, опубликованной в 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".