c# - Micro optimización de DateTime.DayOfWeek
performance micro-optimization (1)
Hagamos algunos ajustes.
- Prime factorización de
TimeSpan.TicksPerDay
(864000000000)
:
DayOfWeek
ahora se puede expresar 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<DateTime> 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í:
╔══════════════════════╦════════════════════╦═════════════════════╦═════════════╗
║ 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 elnew 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<DateTime> 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 sí
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
╔══════════════════════╦════════════════════╦════════════════════════╦═════════════╗
║ 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
Ante todo:
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).
El método
DateTime.DayOfWeek
no representa un cuello de botella en ninguna aplicación mía.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.
Descompilando 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:
Las marcas correspondientes al día actual se dividen por la cantidad existente de tics en un día.
Agregamos 1 al resultado anterior, para que el resto 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?