¿Por qué el interruptor de Java en las entradas contiguas parece ejecutarse más rápido con los casos agregados?
Estoy trabajando en un código Java que necesita ser altamente optimizado, ya que se ejecutará en funciones activas que se invocan en muchos puntos en la lógica de mi programa principal. Parte de este código consiste en multiplicardouble
variables por10
elevado a arbitrario no negativoint
exponent
s. Una forma rápida (edición: pero no la más rápida posible, consulte la Actualización 2 a continuación) para obtener el valor multiplicado esswitch
sobre elexponent
:
double multiplyByPowerOfTen(final double d, final int exponent) {
switch (exponent) {
case 0:
return d;
case 1:
return d*10;
case 2:
return d*100;
// ... same pattern
case 9:
return d*1000000000;
case 10:
return d*10000000000L;
// ... same pattern with long literals
case 18:
return d*1000000000000000000L;
default:
throw new ParseException("Unhandled power of ten " + power, 0);
}
}
Las elipsis comentadas arriba indican quecase
int
las constantes continúan incrementándose en 1, por lo que hay realmente 19case
s en el fragmento de código anterior. Como no estaba seguro de si realmente necesitaría todos los poderes de 10 encase
declaraciones10
a través de18
, Corrí algunos microbenchmarks comparando el tiempo para completar 10 millones de operaciones con esteswitch
declaración versus unswitch
con solocase
s 0
a través de9
(con elexponent
Limitado a 9 o menos para evitar romper el recorte.switch
). Obtuve el resultado bastante sorprendente (¡para mí, al menos!) De que cuanto más tiemposwitch
con máscase
declaraciones realmente corrieron más rápido.
En una alondra, traté de añadir aún máscase
s que acaba de devolver valores ficticios y descubrió que podía hacer que el interruptor se ejecutara aún más rápido con alrededor de 22-27 declaradoscase
s (aunque esos casos ficticios nunca son realmente afectados mientras el código se está ejecutando). (Otra vez,case
s fueron agregados de una manera contigua mediante el incremento de la anteriorcase
constante por1
.) Estas diferencias de tiempo de ejecución no son muy significativas: para un azarexponent
Entre0
y10
, el muñeco acolchadoswitch
La declaración finaliza 10 millones de ejecuciones en 1.49 segundos, en comparación con 1.54 segundos para la versión sin relleno, para un total de ahorros de 5 ns por ejecución. Entonces, no es el tipo de cosa que hace que obsesionarse con el relleno de unswitch
Declaración vale la pena el esfuerzo desde un punto de vista de optimización. Pero todavía me parece curioso y contraintuitivo que unswitch
no se vuelve más lento (o tal vez en el mejor de los casos se mantiene constanteO (1) tiempo) para ejecutar como mascase
s se le añaden.
Estos son los resultados que obtuve de la ejecución con varios límites en los generados aleatoriamente.exponent
valores. No incluí los resultados hasta el final1
Para elexponent
límite, pero la forma general de la curva sigue siendo la misma, con una cresta alrededor de la marca de 12-17, y un valle entre 18-28. Todas las pruebas se ejecutaron en JUnitBenchmarks utilizando contenedores compartidos para los valores aleatorios para garantizar entradas de prueba idénticas. También hice las pruebas tanto en orden como desde las más largas.switch
declaración a la más corta, y viceversa, para tratar de eliminar la posibilidad de ordenar problemas de prueba relacionados. He puesto mi código de prueba en un repositorio de github si alguien quiere intentar reproducir estos resultados.
Entonces, ¿qué está pasando aquí? ¿Algunos caprichos de mi arquitectura o construcción de micro-benchmark? O es el javaswitch
Realmente un poco más rápido de ejecutar en el18
a28
case
rango de lo que es11
hasta17
?
prueba de github "interruptor-experimento"
ACTUALIZAR: Limpié bastante la biblioteca de benchmarking y agregué un archivo de texto en / results con algo de salida en una gama más amplia de posiblesexponent
valores. También agregué una opción en el código de prueba para no lanzar unException
desdedefault
, pero esto no parece afectar los resultados.
ACTUALIZACIÓN 2: Encontré una buena discusión sobre este tema en 2009 en el foro de xkcd aquí:http://forums.xkcd.com/viewtopic.php?f=11&t=33524. La discusión del OP sobre el uso deArray.binarySearch()
Me dio la idea de una implementación simple basada en matrices del patrón de exponenciación anterior. No hay necesidad de la búsqueda binaria ya que sé cuáles son las entradas en elarray
son. Parece funcionar 3 veces más rápido que usarswitch
, obviamente a expensas de algunos de los flujos de control queswitch
permite Ese código se ha añadido al repositorio de github también.