c# - ¿Por qué DateTime.Now DateTime.UtcNow es tan lento/caro?
datetime.utcnow python (3)
Me doy cuenta de que esto está demasiado lejos en el área de microoptimización, pero tengo curiosidad por entender por qué las Llamadas a DateTime.Now y DateTime.UtcNow son tan "caras". Tengo un programa de muestra que ejecuta un par de escenarios de hacer algún "trabajo" (agregar a un contador) e intenta hacer esto durante 1 segundo. Se me han acercado varias maneras de hacer que funcione por un tiempo limitado. Los ejemplos muestran que DateTime.Now y DateTime.UtcNow son significativamente más lentos que Environment.TickCount, pero incluso eso es lento en comparación con solo dejar un hilo separado en reposo durante 1 segundo y luego establecer un valor para indicar que el hilo trabajador se detenga.
Así que mis preguntas son estas:
- Sé que UtcNow es más rápido porque no tiene información de zona horaria, ¿por qué sigue siendo mucho más lento que TickCount?
- ¿Por qué leer un booleano es más rápido que un int?
- ¿Cuál es la forma ideal de lidiar con estos tipos de escenarios en los que necesita permitir que algo se ejecute durante un período de tiempo limitado, pero no quiere perder más tiempo comprobando el tiempo que haciendo el trabajo?
Por favor, perdone la verbosidad del ejemplo:
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;
}
}
FWIW aquí es un código que NLog utiliza para obtener la marca de tiempo para cada mensaje de registro. En este caso, el "trabajo" es la recuperación real de la hora actual (concedida, ocurre en el contexto de un poco más caro de "trabajo", el registro de un mensaje). NLog minimiza el costo de obtener la hora actual al obtener solo la hora "real" (a través de DateTime.Now
) si el recuento de tics actual es diferente del recuento de tics anterior. Esto realmente no se aplica directamente a su pregunta, pero es una forma interesante de "acelerar" la recuperación del tiempo actual.
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;
En el contexto de su pregunta, probablemente podría "mejorar" el rendimiento de su código de bucle de tiempo como este:
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); }
}
Si se llama a CurrentTimeGetter.Now
tanta frecuencia que el tiempo devuelto sería el mismo muchas veces seguidas, solo se debe pagar el costo de Environment.TickCount
. No puedo decir si realmente ayuda con el rendimiento del registro de NLog, tal como lo notaría o no.
No sé si realmente ayuda con su pregunta, o si ya necesita ayuda, pero pensé que sería un ejemplo interesante de aprovechar una operación más rápida ( Environment.Ticks
) para acelerar potencialmente un proceso relativamente lento. Operación ( DateTime.Now
) en algunas circunstancias.
Por lo que puedo decir, DateTime.UtcNow
(no debe confundirse con DateTime.Now
, que es mucho más lento) es la forma más rápida de obtener tiempo. De hecho, el almacenamiento en caché de la forma en que @wageoghe propone disminuye significativamente el rendimiento (en mis pruebas, que fueron 3.5 veces).
En ILSpy, UtcNow se ve así:
[__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));
}
}
Creo que esto sugiere que la función está integrada por el compilador para lograr la máxima velocidad. Puede haber maneras más rápidas de obtener tiempo, pero hasta ahora, no he visto una
TickCount
solo lee un contador en constante aumento. Es lo más simple que puedes hacer.
DateTime.UtcNow
necesita consultar la hora del sistema, y no olvide que si bien TickCount
ignora a la TickCount
cosas como que el usuario cambia el reloj o NTP, UtcNow
debe tener esto en cuenta.
Ahora ha expresado un problema de rendimiento, pero en los ejemplos que ha dado, todo lo que está haciendo es incrementar un contador. Espero que en su código real , estará haciendo más trabajo que eso. Si está haciendo una cantidad significativa de trabajo, es probable que empequeñezca el tiempo empleado por UtcNow
. Antes de hacer cualquier otra cosa, debe medir eso para saber si en realidad está tratando de resolver un problema que no existe.
Si necesitas mejorar las cosas, entonces:
- Puedes usar un temporizador en lugar de crear un nuevo hilo explícitamente. Hay varios tipos de temporizadores en el marco, y sin conocer su situación exacta, no puedo recomendar cuál sería el más adecuado para usar, pero parece una solución mejor que comenzar un hilo.
- Puede medir algunas iteraciones de su tarea y luego adivinar cuántas serán realmente necesarias. Es posible que desee ejecutar la mitad de esas iteraciones, hacer un balance de cuánto tiempo se toma, y luego ajustar el número de ciclos restantes en consecuencia. Por supuesto, esto no funciona si el tiempo de espera por iteración puede variar enormemente.