Micro optimización de DateTime.DayOfWeek

Ante todo:

  1. Estoy haciendo esta pregunta solo por diversión y con muchas ganas de aprender. Tengo que admitir que me encanta meterme con las micro-optimizaciones (aunque nunca me han llevado a un aumento significativo de la velocidad en ninguno de mis desarrollos).

  2. El método DateTime.DayOfWeek no representa un cuello de botella en ninguna aplicación mía.

  3. Y es muy poco probable que sea un problema en cualquier otro. Si alguien está pensando que este método tiene un impacto en el rendimiento de su aplicación, debe pensar en cuándo optimizar y luego, debe realizar un perfil.

Descomstackndo la clase DateTime con ILSpy, descubrimos cómo se implementa DateTime.DayOfWeek :

 [__DynamicallyInvokable] public DayOfWeek DayOfWeek { [__DynamicallyInvokable, TargetedPatchingOptOut("Performance critical to inline across NGen image boundaries")] get { return (DayOfWeek)((this.InternalTicks / 864000000000L + 1L) % 7L); } } public long Ticks { [__DynamicallyInvokable, TargetedPatchingOptOut("Performance critical to inline this type of method across NGen image boundaries")] get { return this.InternalTicks; } } 

Este método realiza lo siguiente:

  1. Las marcas correspondientes al día actual se dividen por la cantidad existente de tics en un día.

  2. Agregamos 1 al resultado anterior, para que el rest de la división de 7 se encuentre entre los números 0 y 6.

¿Es esta la única forma de calcular el día de la semana?

¿Sería posible volver a implementar esto para que funcione más rápido?

Hagamos algunos ajustes.

  1. Prime factorización de TimeSpan.TicksPerDay (864000000000) : Factorización Prime de 864000000000

DayOfWeek ahora se puede express como:

 public DayOfWeek DayOfWeek { get { return (DayOfWeek)(((Ticks>>14) / 52734375 + 1L) % 7L); } } 

Y estamos trabajando en el módulo 7, 52734375 % 7 es 1. Por lo tanto, el código anterior es igual a:

 public static DayOfWeek dayOfWeekTurbo(this DateTime date) { return (DayOfWeek)(((date.Ticks >> 14) + 1) % 7); } 

Intuitivamente, funciona. Pero probémoslo con código

 public static void proof() { DateTime date = DateTime.MinValue; DateTime max_date = DateTime.MaxValue.AddDays(-1); while (date < max_date) { if (date.DayOfWeek != date.dayOfWeekTurbo()) { Console.WriteLine("{0}\t{1}", date.DayOfWeek, date.dayOfWeekTurbo()); Console.ReadLine(); } date = date.AddDays(1); } } 

Puede ejecutarlo si lo desea, pero le aseguro que funciona bien.

Ok, lo único que queda es un poco de evaluación comparativa.

Este es un método auxiliar, para hacer que el código sea más claro:

 public static IEnumerable getAllDates() { DateTime d = DateTime.MinValue; DateTime max = DateTime.MaxValue.AddDays(-1); while (d < max) { yield return d; d = d.AddDays(1); } } 

Supongo que no necesita explicación.

 public static void benchDayOfWeek() { DateTime[] dates = getAllDates().ToArray(); // for preventing the compiler doing things that we don't want to DayOfWeek[] foo = new DayOfWeek[dates.Length]; for (int max_loop = 0; max_loop < 10000; max_loop+=100) { Stopwatch st1, st2; st1 = Stopwatch.StartNew(); for (int i = 0; i < max_loop; i++) for (int j = 0; j < dates.Length; j++) foo[j] = dates[j].DayOfWeek; st1.Stop(); st2 = Stopwatch.StartNew(); for (int i = 0; i < max_loop; i++) for (int j = 0; j < dates.Length; j++) foo[j] = dates[j].dayOfWeekTurbo(); st2.Stop(); Console.WriteLine("{0},{1}", st1.ElapsedTicks, st2.ElapsedTicks); } Console.ReadLine(); Console.WriteLine(foo[0]); } 

Salida:

 96,28 172923452,50884515 352004290,111919170 521851120,168153321 683972846,215554958 846791857,264187194 1042803747,328459950 Monday 

Si hacemos un cuadro con los datos, se ve así:

Gráfico

 ╔══════════════════════╦════════════════════╦═════════════════════╦═════════════╗ ║ Number of iterations ║ Standard DayOfWeek ║ Optimized DayOfWeek ║ Speedup ║ ╠══════════════════════╬════════════════════╬═════════════════════╬═════════════╣ ║ 0 ║ 96 ║ 28 ║ 3.428571429 ║ ║ 100 ║ 172923452 ║ 50884515 ║ 3.398351188 ║ ║ 200 ║ 352004290 ║ 111919170 ║ 3.145165301 ║ ║ 300 ║ 521851120 ║ 168153321 ║ 3.103424404 ║ ║ 400 ║ 683972846 ║ 215554958 ║ 3.1730787 ║ ║ 500 ║ 846791857 ║ 264187194 ║ 3.205272156 ║ ║ 600 ║ 1042803747 ║ 328459950 ║ 3.174827698 ║ ╚══════════════════════╩════════════════════╩═════════════════════╩═════════════╝ 

3 veces más rápido.

Nota: el código se compiló con Visual Studio 2013, modo de lanzamiento, y se ejecutó con todo cerrado excepto la aplicación. (Incluyendo VS, por supuesto).

Realicé las pruebas en un toshiba Satellite C660-2JK , procesador Intel® Core ™ i3-2350M y Windows® 7 Home Premium de 64 bits.

EDITAR:

Como Jon Skeet notó, este método puede fallar cuando no está en un límite de fecha.

Debido al comentario de Jon Skeet esta respuesta,

dayOfWeekTurbo puede fallar cuando no está en un límite de fecha. Por ejemplo, considere el new DateTime(2014, 3, 11, 21, 39, 30) : su método cree que es viernes cuando en realidad es martes. El "estamos trabajando en el módulo 7" es el camino equivocado, básicamente ... eliminando esa división adicional, los cambios de día de la semana durante el día .

Decidí editarlo.

Si cambiamos el método de proof() ,

 public static void proof() { DateTime date = DateTime.MinValue; DateTime max_date = DateTime.MaxValue.AddSeconds(-1); while (date < max_date) { if (date.DayOfWeek != date.dayOfWeekTurbo2()) { Console.WriteLine("{0}\t{1}", date.DayOfWeek, date.dayOfWeekTurbo2()); Console.ReadLine(); } date = date.AddSeconds(1); } } 

¡Falla!

Jon Skeet tenía razón. Sigamos el consejo de Jon Skeet y apliquemos la división.

 public static DayOfWeek dayOfWeekTurbo2(this DateTime date) { return (DayOfWeek)((((date.Ticks >> 14) / 52734375L )+ 1) % 7); } 

Además, cambiamos el método getAllDates() .

 public static IEnumerable getAllDates() { DateTime d = DateTime.MinValue; DateTime max = DateTime.MaxValue.AddHours(-1); while (d < max) { yield return d; d = d.AddHours(1); } } 

Y benchDayOfWeek()

 public static void benchDayOfWeek() { DateTime[] dates = getAllDates().ToArray(); DayOfWeek[] foo = new DayOfWeek[dates.Length]; for (int max_loop = 0; max_loop < 10000; max_loop ++) { Stopwatch st1, st2; st1 = Stopwatch.StartNew(); for (int i = 0; i < max_loop; i++) for (int j = 0; j < dates.Length; j++) foo[j] = dates[j].DayOfWeek; st1.Stop(); st2 = Stopwatch.StartNew(); for (int i = 0; i < max_loop; i++) for (int j = 0; j < dates.Length; j++) foo[j] = dates[j].dayOfWeekTurbo2(); st2.Stop(); Console.WriteLine("{0},{1}", st1.ElapsedTicks, st2.ElapsedTicks); } Console.ReadLine(); Console.WriteLine(foo[0]); } 

¿Todavía será más rápido? la respuesta es

Salida:

 90,26 43772675,17902739 84299562,37339935 119418847,47236771 166955278,72444714 207441663,89852249 223981096,106062643 275440586,125110111 327353547,145689642 363908633,163442675 407152133,181642026 445141584,197571786 495590201,217373350 520907684,236609850 511052601,217571474 610024381,260208969 637676317,275558318 

Gráfico

 ╔══════════════════════╦════════════════════╦════════════════════════╦═════════════╗ ║ Number of iterations ║ Standard DayOfWeek ║ Optimized DayOfWeek(2) ║ Speedup ║ ╠══════════════════════╬════════════════════╬════════════════════════╬═════════════╣ ║ 1 ║ 43772675 ║ 17902739 ║ 2.445026708 ║ ║ 2 ║ 84299562 ║ 37339935 ║ 2.257624766 ║ ║ 3 ║ 119418847 ║ 47236771 ║ 2.528090817 ║ ║ 4 ║ 166955278 ║ 72444714 ║ 2.304588821 ║ ║ 5 ║ 207441663 ║ 89852249 ║ 2.308697504 ║ ║ 6 ║ 223981096 ║ 106062643 ║ 2.111781205 ║ ║ 7 ║ 275440586 ║ 125110111 ║ 2.201585338 ║ ║ 8 ║ 327353547 ║ 145689642 ║ 2.246923958 ║ ║ 9 ║ 363908633 ║ 163442675 ║ 2.226521519 ║ ║ 10 ║ 407152133 ║ 181642026 ║ 2.241508433 ║ ║ 11 ║ 445141584 ║ 197571786 ║ 2.25306251 ║ ║ 12 ║ 495590201 ║ 217373350 ║ 2.279903222 ║ ║ 13 ║ 520907684 ║ 236609850 ║ 2.201546909 ║ ║ 14 ║ 511052601 ║ 217571474 ║ 2.348895246 ║ ║ 15 ║ 610024381 ║ 260208969 ║ 2.344363391 ║ ║ 16 ║ 637676317 ║ 275558318 ║ 2.314124725 ║ ╚══════════════════════╩════════════════════╩════════════════════════╩═════════════╝ 

2 veces más rápido