2010-11-02 20 views
14

Me doy cuenta de que esto es demasiado lejos en el área de la micro-optimización, pero tengo curiosidad por entender por qué las llamadas a DateTime.Now y DateTime.UtcNow son tan "caras" . Tengo un programa de ejemplo que ejecuta un par de escenarios de hacer un "trabajo" (agregar a un contador) e intenta hacer esto durante 1 segundo. Me he dirigido a varios para que haga el trabajo 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 que un hilo separado duerma durante 1 segundo y luego establecer un valor para indicar que el hilo de trabajo se detenga.¿Por qué DateTime.Now DateTime.UtcNow tan lento/caro?

Así que mis preguntas son las siguientes:

  • Sé que UtcNow es más rápido porque no tiene información de zona horaria, ¿por qué es todavía 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 este tipo de escenarios donde necesita permitir que algo se ejecute durante un tiempo limitado, pero no quiere perder más tiempo controlando el tiempo que realmente haciendo el trabajo?

Por favor, perdón por el nivel de detalle 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; 
    } 

} 

Respuesta

17

TickCount simplemente lee un contador en constante aumento. Es casi lo más simple que puedes hacer.

DateTime.UtcNow necesita para consultar la hora del sistema - y no se olvide que, si bien TickCount es felizmente ignorante de cosas como el usuario cambiar el reloj o NTP, UtcNow tiene que tener esto en cuenta.

Ahora ha expresado una preocupación por el rendimiento, pero en los ejemplos que ha dado, todo lo que hace es aumentar el contador. Esperaría que en tu código real, harás bastante más trabajo que eso. Si está haciendo una cantidad significativa de trabajo , es probable que empequeñezca el tiempo tomado por UtcNow. Antes de hacer cualquier otra cosa, debe medir eso para averiguar si realmente está tratando de resolver un problema que no existe.

Si haces necesidad de mejorar las cosas, entonces:

  • Se puede utilizar un temporizador en lugar de crear un nuevo hilo de forma explícita. Existen varios tipos de temporizadores en el marco y, sin conocer su situación exacta, no puedo aconsejar sobre cuál sería el más sensato de usar, pero parece una solución mejor que iniciar un hilo.
  • Puede medir algunas iteraciones de su tarea, luego adivinar cuántas se necesitarán en realidad. Es posible que desee ejecutar la mitad de tantas iteraciones, hacer un inventario de cuánto tiempo se tarda, luego ajustar el número de ciclos restantes en consecuencia. Por supuesto, esto no funciona si el tiempo tomado por iteración puede variar enormemente.
+0

Gracias Jon. Investigaré los temporizadores. Me doy cuenta de que el "trabajo" en mi ejemplo no es realista. Sin embargo, tenía curiosidad de saber de dónde venía el gran impacto. –

+1

@My Other Me: Básicamente, en comparación con el trabajo de incrementar un contador, casi * cualquier * trabajo contará como un gran impacto :) –

15

FWIW Aquí hay algunos códigos que NLog usa 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, sucede en el contexto de un poco probablemente un poco más caro de "trabajo", el registro de un mensaje).NLog minimiza el costo de obtener la hora actual solo obteniendo el tiempo "real" (a través de DateTime.Now) si el conteo de ticks actual es diferente al conteo de ticks anterior. Esto realmente no se aplica directamente a su pregunta, pero es una forma interesante de "acelerar" la recuperación de 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, que probablemente podría "mejorar" el rendimiento de su código de bucle momento 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 CurrentTimeGetter.Now se llama con tanta frecuencia que la hora devuelta sería el el mismo número de veces seguidas, solo se debe pagar el costo de Environment.TickCount. No puedo decir si realmente ayuda con el rendimiento del registro de NLog, de modo que lo notarías o no.

No sé si realmente ayuda en su pregunta, o si incluso necesita más ayuda, pero pensé que sería un ejemplo interesante de aprovechar una operación más rápida (Environment.Ticks) para acelerar potencialmente una operación relativamente lenta (DateTime.Now) en algunas circunstancias.

+0

según mis pruebas, la función proporcionada es 3.5 veces más lenta que simplemente consultar DateTime. UtcNow –

2

Por lo que puedo decir, DateTime.UtcNow (que no debe confundirse con DateTime.Now, que es mucho más lento) es la forma más rápida de obtener tiempo. De hecho, almacenarlo en caché de la forma en que @wageoghe propone disminuye el rendimiento de manera significativa (en mis pruebas, eso fue 3,5 veces).

En ILSpy, UtcNow se parece a esto:

[__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)); 
    } 
} 

pienso, esto sugiere que la función se colocarán en línea por el compilador para lograr la máxima velocidad. Puede haber formas más rápidas de obtener tiempo, pero hasta ahora, no he visto ninguno