Почему DateTime.Now DateTime.UtcNow так медленно/дорого
Я понимаю, что это слишком далеко в области микро-оптимизации, но мне любопытно понять, почему Calls to 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;
}
}
Ответы
Ответ 1
TickCount
просто читает постоянно увеличивающийся счетчик. Это просто простейшая вещь, которую вы можете сделать.
DateTime.UtcNow
необходимо запросить системное время - и не забывайте, что в то время как TickCount
блаженно игнорирует такие вещи, как пользователь, изменяющий часы, или NTP, UtcNow
должен учитывать это.
Теперь вы выразили озабоченность по поводу производительности, но в приведенных вами примерах все, что вы делаете, увеличивает счетчик. Я бы ожидал, что в вашем реальном коде вы будете делать больше работы, чем это. Если вы выполняете значительную работу, это может затмить время, затраченное на UtcNow
. Прежде чем делать что-либо еще, вы должны измерить это, чтобы выяснить, действительно ли вы пытаетесь решить проблему, которая не существует.
Если вам нужно улучшить ситуацию, то:
- Вы можете использовать таймер, а не создавать новый поток явно. В рамке существуют различные типы таймеров, и, не зная вашей конкретной ситуации, я не могу советовать, на что было бы разумнее использовать, - но это похоже на лучшее решение, чем на запуск потока.
- Вы можете измерить несколько итераций своей задачи, а затем угадать, сколько из них действительно потребуется. Возможно, вам захочется выполнить половину этого количества итераций, подсчитать, сколько времени это займет, а затем настроить количество оставшихся циклов соответственно. Конечно, это не работает, если время на итерацию может сильно различаться.
Ответ 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
) в некоторых случаях.
Ответ 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
см. эту нить с сетью, где BenchmarkDotNet
использовался для профилирования.
К сожалению, была совершенная регрессия с переходом на .NET(Core) 3 по сравнению с 2.2, но даже с отчетом о регрессионном значении DateTime.UtcNow
приходит в довольно удачное время 71 ns
(это было 25 ns
).), т.е. 71 миллиардная доля секунды.
Чтобы представить это в перспективе, даже на более медленной скорости 71ns
, это означает:
Вы можете позвонить DateTime.UtcNow
~ 14 000 раз по цене всего 1 миллисекунды!
В ранее более быстрое время 25 ns
(надеюсь, они вернут эту производительность), вы можете позвонить DateTime.UtcNow
~ 40000 раз за 1 миллисекунду.
Я не смотрю на старые времена .NET Framework здесь, но, по крайней мере, с новыми битами, я думаю, можно смело утверждать, что, по крайней мере, уже неточно говорить, что DateTime.UtcNow
"медленный/дорогой" (я ценим, что вопрос был задан однако!).