¿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 exponents. 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 19cases 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 solocases 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áscases 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 declaradoscases (aunque esos casos ficticios nunca son realmente afectados mientras el código se está ejecutando). (Otra vez,cases 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 mascases 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.

Respuestas a la pregunta(4)

Su respuesta a la pregunta