¿Por qué el cambio de Java en ints contiguos parece ejecutarse más rápido con casos agregados?

276

Estoy trabajando en algún código Java que necesita ser altamente optimizado ya que se ejecutará en funciones activas que se invocan en muchos puntos de la lógica de mi programa principal. Parte de este código implica multiplicar doublevariables por 10elevadas a s arbitrarias no negativas int exponent. Una forma rápida (editar: pero no la más rápida posible, consulte la Actualización 2 a continuación) para obtener el valor multiplicado es switchen exponent:

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 elipses comentadas arriba indican que las case intconstantes continúan incrementándose en 1, por lo que realmente hay 19 cases en el fragmento de código anterior. Ya que no estaba seguro de si iba a necesitar realmente todas las potencias de 10, en casedeclaraciones 10a través 18, me encontré con algunos microbenchmarks que comparan el tiempo para completar 10 millones de operaciones con esta switchdeclaración frente a un switchsólo cases 0thru 9(con la exponentlimitación a 9 o menos a evite romper el recortado switch). Obtuve el resultado bastante sorprendente (¡para mí, al menos!) De que cuanto más tiempo switchcon más casedeclaraciones realmente se ejecutaba más rápido.

En una alondra, intenté agregar aún más cases que solo devolvieron valores ficticios, y descubrí que podía hacer que el interruptor se ejecutara aún más rápido con alrededor de 22-27 cases declarados (a pesar de que esos casos ficticios nunca se golpean mientras el código se está ejecutando ) (Nuevamente, lacase se agregaron s de manera contigua incrementando la caseconstante anterior en 1). Estas diferencias en el tiempo de ejecución no son muy significativas: para un azar exponententre 0y 10, la switchinstrucción ficticia completa termina 10 millones de ejecuciones en 1.49 segundos versus 1.54 segundos para las sin relleno versión, para un gran ahorro total de 5ns por ejecución. Entonces, no es el tipo de cosa que hace que obsesionarse con rellenar unswitchdeclaración que vale la pena desde el punto de vista de la optimización. Pero todavía me parece curioso y contraintuitivo que a switchno se vuelve más lento (o tal vez, en el mejor de los casos, mantiene un tiempo O (1) constante ) para ejecutarse a medida que casese agregan más s.

cambiar los resultados de la evaluación comparativa

Estos son los resultados que obtuve al ejecutar con varios límites en el generado aleatoriamente exponent valores . No incluí los resultados hasta 1el exponentlímite, pero la forma general de la curva sigue siendo la misma, con una cresta alrededor de la marca del caso 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 ejecuté las pruebas en orden, desde la switchdeclaración más larga hasta la más corta, y viceversa, para tratar de eliminar la posibilidad de problemas de prueba relacionados con el pedido. 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 Java switchen realidad un poco más rápido para ejecutar en el 18que28 case rango de lo que es de 11hasta 17?

prueba de github repo "interruptor-experimento"

ACTUALIZACIÓN: Limpié bastante la biblioteca de evaluación comparativa y agregué un archivo de texto en / resultados con algo de salida en un rango más amplio de exponentvalores posibles . También he añadido una opción en el código de prueba no lanzar una Exceptionde default, pero esto no parece afectar a los resultados.

ACTUALIZACIÓN 2: Encontré una discusión bastante buena sobre este tema desde 2009 en el foro xkcd aquí: http://forums.xkcd.com/viewtopic.php?f=11&t=33524 . La discusión del OP sobre el uso Array.binarySearch()me dio la idea de una implementación simple basada en una matriz del patrón de exponenciación anterior. No hay necesidad de la búsqueda binaria ya que sé cuáles son las entradas en el array. Parece correr aproximadamente 3 veces más rápido que el uso switch, obviamente a expensas de parte del flujo de control que switchofrece. Ese código también se ha agregado al repositorio de github.

Andrew Bissell
fuente
64
Ahora todos los Googlers en todas partes tendrán exactamente 22 casos en todas las switchdeclaraciones, ya que es claramente la solución más óptima. : D (No mostrar esto a mi ventaja, por favor.)
asteri
2
¿Tienes un SSCCE más simple? Este no compila para mí. Tan débil como soy con el rendimiento de Java, quiero intentarlo.
Mysticial
55
Puede encontrar útil la sección "Interruptores en la JVM" en mi respuesta sobre casos basados ​​en cadenas. Creo que lo que está sucediendo aquí es que estás cambiando de lookupswitcha a tableswitch. Desmontar su código con javaple mostraría seguro.
Erickson
2
Agregué los tarros de dependencia a la carpeta / lib en el repositorio. @Mysticial Lo siento, ¡ya pasé demasiado tiempo bajando por esta madriguera de conejos! Si quita el "extiende AbstractBenchmark" de las clases de prueba y se deshace de las importaciones de "com.carrotsearch", puede ejecutar solo con la dependencia JUnit, pero el material de zanahoria es bastante bueno para filtrar parte del ruido del JIT y períodos de calentamiento. Lamentablemente, no sé cómo ejecutar estas pruebas JUnit fuera de IntelliJ.
Andrew Bissell
2
@ AndrewBissell Logré reprogramar tus resultados con un punto de referencia mucho más simple. La rama frente a la tabla para el rendimiento de tamaño pequeño frente a mediano era una suposición algo obvia. Pero no tengo una mejor idea que nadie sobre la inmersión en 30 casos ...
Mysticial

Respuestas:

228

Como se señaló en la otra respuesta , dado que los valores de los casos son contiguos (en lugar de dispersos), el código de bytes generado para sus diversas pruebas utiliza una tabla de interruptores (instrucción de código de bytes tableswitch).

Sin embargo, una vez que el JIT comienza su trabajo y compila el código de bytes en el ensamblaje, la tableswitchinstrucción no siempre da como resultado una matriz de punteros: a veces, la tabla de interruptores se transforma en lo que parece un lookupswitch(similar a una estructura if/ else if).

La descomposición del ensamblaje generado por el JIT (punto de acceso JDK 1.7) muestra que usa una sucesión de if / else if cuando hay 17 casos o menos, una matriz de punteros cuando hay más de 18 (más eficiente).

La razón por la cual se usa este número mágico de 18 parece reducirse al valor predeterminado de la MinJumpTableSizebandera JVM (alrededor de la línea 352 en el código).

He planteado el problema en la lista del compilador de puntos de acceso y parece ser un legado de pruebas anteriores . Tenga en cuenta que este valor predeterminado se ha eliminado en JDK 8 después de realizar más evaluaciones comparativas .

Finalmente, cuando el método se vuelve demasiado largo (> 25 casos en mis pruebas), ya no está alineado con la configuración predeterminada de JVM, esa es la causa más probable de la caída del rendimiento en ese punto.


Con 5 casos, el código descompilado se ve así (observe las instrucciones cmp / je / jg / jmp, el ensamblaje para if / goto):

[Verified Entry Point]
  # {method} 'multiplyByPowerOfTen' '(DI)D' in 'javaapplication4/Test1'
  # parm0:    xmm0:xmm0   = double
  # parm1:    rdx       = int
  #           [sp+0x20]  (sp of caller)
  0x00000000024f0160: mov    DWORD PTR [rsp-0x6000],eax
                                                ;   {no_reloc}
  0x00000000024f0167: push   rbp
  0x00000000024f0168: sub    rsp,0x10           ;*synchronization entry
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@-1 (line 56)
  0x00000000024f016c: cmp    edx,0x3
  0x00000000024f016f: je     0x00000000024f01c3
  0x00000000024f0171: cmp    edx,0x3
  0x00000000024f0174: jg     0x00000000024f01a5
  0x00000000024f0176: cmp    edx,0x1
  0x00000000024f0179: je     0x00000000024f019b
  0x00000000024f017b: cmp    edx,0x1
  0x00000000024f017e: jg     0x00000000024f0191
  0x00000000024f0180: test   edx,edx
  0x00000000024f0182: je     0x00000000024f01cb
  0x00000000024f0184: mov    ebp,edx
  0x00000000024f0186: mov    edx,0x17
  0x00000000024f018b: call   0x00000000024c90a0  ; OopMap{off=48}
                                                ;*new  ; - javaapplication4.Test1::multiplyByPowerOfTen@72 (line 83)
                                                ;   {runtime_call}
  0x00000000024f0190: int3                      ;*new  ; - javaapplication4.Test1::multiplyByPowerOfTen@72 (line 83)
  0x00000000024f0191: mulsd  xmm0,QWORD PTR [rip+0xffffffffffffffa7]        # 0x00000000024f0140
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@52 (line 62)
                                                ;   {section_word}
  0x00000000024f0199: jmp    0x00000000024f01cb
  0x00000000024f019b: mulsd  xmm0,QWORD PTR [rip+0xffffffffffffff8d]        # 0x00000000024f0130
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@46 (line 60)
                                                ;   {section_word}
  0x00000000024f01a3: jmp    0x00000000024f01cb
  0x00000000024f01a5: cmp    edx,0x5
  0x00000000024f01a8: je     0x00000000024f01b9
  0x00000000024f01aa: cmp    edx,0x5
  0x00000000024f01ad: jg     0x00000000024f0184  ;*tableswitch
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@1 (line 56)
  0x00000000024f01af: mulsd  xmm0,QWORD PTR [rip+0xffffffffffffff81]        # 0x00000000024f0138
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@64 (line 66)
                                                ;   {section_word}
  0x00000000024f01b7: jmp    0x00000000024f01cb
  0x00000000024f01b9: mulsd  xmm0,QWORD PTR [rip+0xffffffffffffff67]        # 0x00000000024f0128
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@70 (line 68)
                                                ;   {section_word}
  0x00000000024f01c1: jmp    0x00000000024f01cb
  0x00000000024f01c3: mulsd  xmm0,QWORD PTR [rip+0xffffffffffffff55]        # 0x00000000024f0120
                                                ;*tableswitch
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@1 (line 56)
                                                ;   {section_word}
  0x00000000024f01cb: add    rsp,0x10
  0x00000000024f01cf: pop    rbp
  0x00000000024f01d0: test   DWORD PTR [rip+0xfffffffffdf3fe2a],eax        # 0x0000000000430000
                                                ;   {poll_return}
  0x00000000024f01d6: ret    

Con 18 casos, el ensamblaje se ve así (observe la matriz de punteros que se usa y suprime la necesidad de todas las comparaciones: jmp QWORD PTR [r8+r10*1]salta directamente a la multiplicación correcta), esa es la razón probable para la mejora del rendimiento:

[Verified Entry Point]
  # {method} 'multiplyByPowerOfTen' '(DI)D' in 'javaapplication4/Test1'
  # parm0:    xmm0:xmm0   = double
  # parm1:    rdx       = int
  #           [sp+0x20]  (sp of caller)
  0x000000000287fe20: mov    DWORD PTR [rsp-0x6000],eax
                                                ;   {no_reloc}
  0x000000000287fe27: push   rbp
  0x000000000287fe28: sub    rsp,0x10           ;*synchronization entry
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@-1 (line 56)
  0x000000000287fe2c: cmp    edx,0x13
  0x000000000287fe2f: jae    0x000000000287fe46
  0x000000000287fe31: movsxd r10,edx
  0x000000000287fe34: shl    r10,0x3
  0x000000000287fe38: movabs r8,0x287fd70       ;   {section_word}
  0x000000000287fe42: jmp    QWORD PTR [r8+r10*1]  ;*tableswitch
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@1 (line 56)
  0x000000000287fe46: mov    ebp,edx
  0x000000000287fe48: mov    edx,0x31
  0x000000000287fe4d: xchg   ax,ax
  0x000000000287fe4f: call   0x00000000028590a0  ; OopMap{off=52}
                                                ;*new  ; - javaapplication4.Test1::multiplyByPowerOfTen@202 (line 96)
                                                ;   {runtime_call}
  0x000000000287fe54: int3                      ;*new  ; - javaapplication4.Test1::multiplyByPowerOfTen@202 (line 96)
  0x000000000287fe55: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe8b]        # 0x000000000287fce8
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@194 (line 92)
                                                ;   {section_word}
  0x000000000287fe5d: jmp    0x000000000287ff16
  0x000000000287fe62: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe86]        # 0x000000000287fcf0
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@188 (line 90)
                                                ;   {section_word}
  0x000000000287fe6a: jmp    0x000000000287ff16
  0x000000000287fe6f: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe81]        # 0x000000000287fcf8
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@182 (line 88)
                                                ;   {section_word}
  0x000000000287fe77: jmp    0x000000000287ff16
  0x000000000287fe7c: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe7c]        # 0x000000000287fd00
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@176 (line 86)
                                                ;   {section_word}
  0x000000000287fe84: jmp    0x000000000287ff16
  0x000000000287fe89: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe77]        # 0x000000000287fd08
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@170 (line 84)
                                                ;   {section_word}
  0x000000000287fe91: jmp    0x000000000287ff16
  0x000000000287fe96: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe72]        # 0x000000000287fd10
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@164 (line 82)
                                                ;   {section_word}
  0x000000000287fe9e: jmp    0x000000000287ff16
  0x000000000287fea0: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe70]        # 0x000000000287fd18
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@158 (line 80)
                                                ;   {section_word}
  0x000000000287fea8: jmp    0x000000000287ff16
  0x000000000287feaa: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe6e]        # 0x000000000287fd20
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@152 (line 78)
                                                ;   {section_word}
  0x000000000287feb2: jmp    0x000000000287ff16
  0x000000000287feb4: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe24]        # 0x000000000287fce0
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@146 (line 76)
                                                ;   {section_word}
  0x000000000287febc: jmp    0x000000000287ff16
  0x000000000287febe: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe6a]        # 0x000000000287fd30
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@140 (line 74)
                                                ;   {section_word}
  0x000000000287fec6: jmp    0x000000000287ff16
  0x000000000287fec8: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe68]        # 0x000000000287fd38
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@134 (line 72)
                                                ;   {section_word}
  0x000000000287fed0: jmp    0x000000000287ff16
  0x000000000287fed2: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe66]        # 0x000000000287fd40
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@128 (line 70)
                                                ;   {section_word}
  0x000000000287feda: jmp    0x000000000287ff16
  0x000000000287fedc: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe64]        # 0x000000000287fd48
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@122 (line 68)
                                                ;   {section_word}
  0x000000000287fee4: jmp    0x000000000287ff16
  0x000000000287fee6: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe62]        # 0x000000000287fd50
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@116 (line 66)
                                                ;   {section_word}
  0x000000000287feee: jmp    0x000000000287ff16
  0x000000000287fef0: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe60]        # 0x000000000287fd58
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@110 (line 64)
                                                ;   {section_word}
  0x000000000287fef8: jmp    0x000000000287ff16
  0x000000000287fefa: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe5e]        # 0x000000000287fd60
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@104 (line 62)
                                                ;   {section_word}
  0x000000000287ff02: jmp    0x000000000287ff16
  0x000000000287ff04: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe5c]        # 0x000000000287fd68
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@98 (line 60)
                                                ;   {section_word}
  0x000000000287ff0c: jmp    0x000000000287ff16
  0x000000000287ff0e: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe12]        # 0x000000000287fd28
                                                ;*tableswitch
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@1 (line 56)
                                                ;   {section_word}
  0x000000000287ff16: add    rsp,0x10
  0x000000000287ff1a: pop    rbp
  0x000000000287ff1b: test   DWORD PTR [rip+0xfffffffffd9b00df],eax        # 0x0000000000230000
                                                ;   {poll_return}
  0x000000000287ff21: ret    

Y finalmente, el ensamblaje con 30 casos (a continuación) se parece a 18 casos, excepto por el adicional movapd xmm0,xmm1que aparece hacia la mitad del código, como lo detectó @cHao ; sin embargo, la razón más probable para la caída en el rendimiento es que el método también largo para estar alineado con la configuración predeterminada de JVM:

[Verified Entry Point]
  # {method} 'multiplyByPowerOfTen' '(DI)D' in 'javaapplication4/Test1'
  # parm0:    xmm0:xmm0   = double
  # parm1:    rdx       = int
  #           [sp+0x20]  (sp of caller)
  0x0000000002524560: mov    DWORD PTR [rsp-0x6000],eax
                                                ;   {no_reloc}
  0x0000000002524567: push   rbp
  0x0000000002524568: sub    rsp,0x10           ;*synchronization entry
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@-1 (line 56)
  0x000000000252456c: movapd xmm1,xmm0
  0x0000000002524570: cmp    edx,0x1f
  0x0000000002524573: jae    0x0000000002524592  ;*tableswitch
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@1 (line 56)
  0x0000000002524575: movsxd r10,edx
  0x0000000002524578: shl    r10,0x3
  0x000000000252457c: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe3c]        # 0x00000000025243c0
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@364 (line 118)
                                                ;   {section_word}
  0x0000000002524584: movabs r8,0x2524450       ;   {section_word}
  0x000000000252458e: jmp    QWORD PTR [r8+r10*1]  ;*tableswitch
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@1 (line 56)
  0x0000000002524592: mov    ebp,edx
  0x0000000002524594: mov    edx,0x31
  0x0000000002524599: xchg   ax,ax
  0x000000000252459b: call   0x00000000024f90a0  ; OopMap{off=64}
                                                ;*new  ; - javaapplication4.Test1::multiplyByPowerOfTen@370 (line 120)
                                                ;   {runtime_call}
  0x00000000025245a0: int3                      ;*new  ; - javaapplication4.Test1::multiplyByPowerOfTen@370 (line 120)
  0x00000000025245a1: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe27]        # 0x00000000025243d0
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@358 (line 116)
                                                ;   {section_word}
  0x00000000025245a9: jmp    0x0000000002524744
  0x00000000025245ae: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe22]        # 0x00000000025243d8
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@348 (line 114)
                                                ;   {section_word}
  0x00000000025245b6: jmp    0x0000000002524744
  0x00000000025245bb: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe1d]        # 0x00000000025243e0
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@338 (line 112)
                                                ;   {section_word}
  0x00000000025245c3: jmp    0x0000000002524744
  0x00000000025245c8: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe18]        # 0x00000000025243e8
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@328 (line 110)
                                                ;   {section_word}
  0x00000000025245d0: jmp    0x0000000002524744
  0x00000000025245d5: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe13]        # 0x00000000025243f0
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@318 (line 108)
                                                ;   {section_word}
  0x00000000025245dd: jmp    0x0000000002524744
  0x00000000025245e2: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe0e]        # 0x00000000025243f8
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@308 (line 106)
                                                ;   {section_word}
  0x00000000025245ea: jmp    0x0000000002524744
  0x00000000025245ef: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe09]        # 0x0000000002524400
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@298 (line 104)
                                                ;   {section_word}
  0x00000000025245f7: jmp    0x0000000002524744
  0x00000000025245fc: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe04]        # 0x0000000002524408
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@288 (line 102)
                                                ;   {section_word}
  0x0000000002524604: jmp    0x0000000002524744
  0x0000000002524609: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffdff]        # 0x0000000002524410
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@278 (line 100)
                                                ;   {section_word}
  0x0000000002524611: jmp    0x0000000002524744
  0x0000000002524616: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffdfa]        # 0x0000000002524418
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@268 (line 98)
                                                ;   {section_word}
  0x000000000252461e: jmp    0x0000000002524744
  0x0000000002524623: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffd9d]        # 0x00000000025243c8
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@258 (line 96)
                                                ;   {section_word}
  0x000000000252462b: jmp    0x0000000002524744
  0x0000000002524630: movapd xmm0,xmm1
  0x0000000002524634: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffe0c]        # 0x0000000002524448
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@242 (line 92)
                                                ;   {section_word}
  0x000000000252463c: jmp    0x0000000002524744
  0x0000000002524641: movapd xmm0,xmm1
  0x0000000002524645: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffddb]        # 0x0000000002524428
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@236 (line 90)
                                                ;   {section_word}
  0x000000000252464d: jmp    0x0000000002524744
  0x0000000002524652: movapd xmm0,xmm1
  0x0000000002524656: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffdd2]        # 0x0000000002524430
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@230 (line 88)
                                                ;   {section_word}
  0x000000000252465e: jmp    0x0000000002524744
  0x0000000002524663: movapd xmm0,xmm1
  0x0000000002524667: mulsd  xmm0,QWORD PTR [rip+0xfffffffffffffdc9]        # 0x0000000002524438
                                                ;*dmul
                                                ; - javaapplication4.Test1::multiplyByPowerOfTen@224 (line 86)
                                                ;   {section_word}

[etc.]

  0x0000000002524744: add    rsp,0x10
  0x0000000002524748: pop    rbp
  0x0000000002524749: test   DWORD PTR [rip+0xfffffffffde1b8b1],eax        # 0x0000000000340000
                                                ;   {poll_return}
  0x000000000252474f: ret    
asilias
fuente
77
@ syb0rg Para ser honesto, tampoco entiendo los detalles finos ;-)
assylias
44
+1 para una gran respuesta! ¿Podría desmontar algo con más de 30 casos para comparar cuando el rendimiento sale del "descenso" en la tabla del OP?
asteri
44
@VivinPaliath stackoverflow.com/questions/1503479/…
assylias
2
@ AndrewBissell Supongo que el comportamiento diferente se basa en (i) pruebas de rendimiento de arquitectura cruzada que han demostrado que la matriz de punteros solo es eficiente cuando el número de casos es mayor que 18 o (ii) el código se perfila como se ejecuta y el generador de perfiles determina qué enfoque es mejor durante el tiempo de ejecución. No puedo encontrar la respuesta.
Assylias
3
El desmontaje de 30 cajas y el de 18 cajas se ven casi iguales. Las diferencias parecen en su mayoría limitadas a un poco extra de barajado de registro adicional después del 11 ° caso. No puedo decir por qué el JITter hace eso; Parece innecesario.
cHao
46

Interruptor: el caso es más rápido si los valores del caso se colocan en un rango estrecho, por ejemplo.

case 1:
case 2:
case 3:
..
..
case n:

Porque, en este caso, el compilador puede evitar realizar una comparación para cada tramo de caso en la instrucción switch. El compilador crea una tabla de salto que contiene las direcciones de las acciones que se realizarán en diferentes tramos. El valor en el que se realiza el cambio se manipula para convertirlo en un índice enjump table . En esta implementación, el tiempo empleado en la instrucción switch es mucho menor que el tiempo empleado en una cascada equivalente de instrucciones if-else-if. Además, el tiempo empleado en la declaración de cambio es independiente del número de tramos de casos en la declaración de cambio.

Como se indica en wikipedia sobre la declaración de cambio en la sección de compilación.

Si el rango de valores de entrada es identificablemente 'pequeño' y tiene solo algunas brechas, algunos compiladores que incorporan un optimizador pueden implementar la instrucción switch como una tabla de ramificación o una matriz de punteros de función indexados en lugar de una larga serie de instrucciones condicionales. Esto permite que la instrucción switch determine instantáneamente qué rama ejecutar sin tener que pasar por una lista de comparaciones.

Vishal K
fuente
44
Eso no es correcto. Será más rápido independientemente de que los valores de los casos sean estrechos o de rango amplio. Es O (1), no debería importar cuán separados estén los valores de las mayúsculas y minúsculas.
Aniket Inge
66
@Aniket: Lea este artículo de wikipedia. en.wikipedia.org/wiki/Branch_table
Vishal K
14
@Aniket: No es O (1) si el rango es amplio y disperso. Hay dos tipos de conmutadores, y si el rango está demasiado extendido, Java lo compilará en un "conmutador de búsqueda" en lugar de un "conmutador de tabla". El primero requiere una comparación por rama hasta que se encuentre, mientras que el segundo no.
cHao
44
Wikipedia es un lugar decente para encontrar referencias, pero no debe considerarse una fuente autorizada. Todo lo que lees allí es, en el mejor de los casos, información de segunda mano.
cHao
66
@Aniket: Para ser justos, el desmontaje es específico de una JVM dada en una plataforma específica. Otros pueden traducirlo de manera diferente. De hecho, algunos podrían usar una tabla hash para un interruptor de búsqueda. Todavía no funcionará tan bien como un interruptor de mesa, pero al menos podría estar cerca. JIT tardaría más tiempo e implicaría aplicar un algoritmo hash a la entrada. Entonces, aunque el código de ensamblaje resultante puede ser esclarecedor, tampoco es autoritario a menos que esté hablando específicamente de Hotspot v1.7.cualquier cosa en Windows x86_64.
cHao
30

La respuesta está en el código de bytes:

SwitchTest10.java

public class SwitchTest10 {

    public static void main(String[] args) {
        int n = 0;

        switcher(n);
    }

    public static void switcher(int n) {
        switch(n) {
            case 0: System.out.println(0);
                    break;

            case 1: System.out.println(1);
                    break;

            case 2: System.out.println(2);
                    break;

            case 3: System.out.println(3);
                    break;

            case 4: System.out.println(4);
                    break;

            case 5: System.out.println(5);
                    break;

            case 6: System.out.println(6);
                    break;

            case 7: System.out.println(7);
                    break;

            case 8: System.out.println(8);
                    break;

            case 9: System.out.println(9);
                    break;

            case 10: System.out.println(10);
                    break;

            default: System.out.println("test");
        }
    }       
}

Código de bytes correspondiente; solo se muestran las partes relevantes:

public static void switcher(int);
  Code:
   0:   iload_0
   1:   tableswitch{ //0 to 10
        0: 60;
        1: 70;
        2: 80;
        3: 90;
        4: 100;
        5: 110;
        6: 120;
        7: 131;
        8: 142;
        9: 153;
        10: 164;
        default: 175 }

SwitchTest22.java:

public class SwitchTest22 {

    public static void main(String[] args) {
        int n = 0;

        switcher(n);
    }

    public static void switcher(int n) {
        switch(n) {
            case 0: System.out.println(0);
                    break;

            case 1: System.out.println(1);
                    break;

            case 2: System.out.println(2);
                    break;

            case 3: System.out.println(3);
                    break;

            case 4: System.out.println(4);
                    break;

            case 5: System.out.println(5);
                    break;

            case 6: System.out.println(6);
                    break;

            case 7: System.out.println(7);
                    break;

            case 8: System.out.println(8);
                    break;

            case 9: System.out.println(9);
                    break;

            case 100: System.out.println(10);
                    break;

            case 110: System.out.println(10);
                    break;
            case 120: System.out.println(10);
                    break;
            case 130: System.out.println(10);
                    break;
            case 140: System.out.println(10);
                    break;
            case 150: System.out.println(10);
                    break;
            case 160: System.out.println(10);
                    break;
            case 170: System.out.println(10);
                    break;
            case 180: System.out.println(10);
                    break;
            case 190: System.out.println(10);
                    break;
            case 200: System.out.println(10);
                    break;
            case 210: System.out.println(10);
                    break;

            case 220: System.out.println(10);
                    break;

            default: System.out.println("test");
        }
    }       
}

Código de bytes correspondiente; de nuevo, solo se muestran las partes relevantes:

public static void switcher(int);
  Code:
   0:   iload_0
   1:   lookupswitch{ //23
        0: 196;
        1: 206;
        2: 216;
        3: 226;
        4: 236;
        5: 246;
        6: 256;
        7: 267;
        8: 278;
        9: 289;
        100: 300;
        110: 311;
        120: 322;
        130: 333;
        140: 344;
        150: 355;
        160: 366;
        170: 377;
        180: 388;
        190: 399;
        200: 410;
        210: 421;
        220: 432;
        default: 443 }

En el primer caso, con rangos estrechos, el código de bytes compilado utiliza a tableswitch. En el segundo caso, el bytecode compilado utiliza a lookupswitch.

En tableswitch, el valor entero en la parte superior de la pila se usa para indexar en la tabla, para encontrar el objetivo de rama / salto. Este salto / rama se realiza inmediatamente. Por lo tanto, esta es una O(1)operación.

A lookupswitches más complicado. En este caso, el valor entero debe compararse con todas las claves de la tabla hasta que se encuentre la clave correcta. Una vez que se encuentra la clave, el objetivo de salto / rama (al que se asigna esta clave) se usa para el salto. La tabla que se usa lookupswitchse ordena y se puede usar un algoritmo de búsqueda binaria para encontrar la clave correcta. El rendimiento para una búsqueda binaria es O(log n), y todo el proceso también lo es O(log n), porque el salto todavía está O(1). Entonces, la razón por la que el rendimiento es menor en el caso de rangos dispersos es que primero se debe buscar la clave correcta porque no se puede indexar directamente en la tabla.

Si hay valores dispersos y solo tenía tableswitchque usarlos, la tabla esencialmente contendría entradas ficticias que apuntan a la defaultopción. Por ejemplo, suponiendo que la última entrada SwitchTest10.javafue en 21lugar de 10, obtienes:

public static void switcher(int);
  Code:
   0:   iload_0
   1:   tableswitch{ //0 to 21
        0: 104;
        1: 114;
        2: 124;
        3: 134;
        4: 144;
        5: 154;
        6: 164;
        7: 175;
        8: 186;
        9: 197;
        10: 219;
        11: 219;
        12: 219;
        13: 219;
        14: 219;
        15: 219;
        16: 219;
        17: 219;
        18: 219;
        19: 219;
        20: 219;
        21: 208;
        default: 219 }

Entonces, el compilador básicamente crea esta enorme tabla que contiene entradas ficticias entre los huecos, apuntando al objetivo de la rama de la defaultinstrucción. Incluso si no hay un default, contendrá entradas que apunten a la instrucción después del bloqueo del interruptor. Hice algunas pruebas básicas y descubrí que si la brecha entre el último índice y el anterior ( 9) es mayor que 35, se usa a en lookupswitchlugar de a tableswitch.

El comportamiento de la switchdeclaración se define en la Especificación de máquina virtual Java (§3.10) :

Cuando los casos del interruptor son escasos, la representación de la tabla de la instrucción del interruptor de mesa se vuelve ineficiente en términos de espacio. La instrucción lookupswitch se puede usar en su lugar. La instrucción lookupswitch empareja las teclas int (los valores de las etiquetas de los casos) con los desplazamientos de destino en una tabla. Cuando se ejecuta una instrucción lookupswitch, el valor de la expresión del switch se compara con las teclas de la tabla. Si una de las claves coincide con el valor de la expresión, la ejecución continúa en el desplazamiento objetivo asociado. Si no coincide ninguna clave, la ejecución continúa en el objetivo predeterminado. [...]

Vivin Paliath
fuente
1
Comprendí a partir de la pregunta que los números siempre son contiguos, pero el rango es más o menos largo, es decir, en un ejemplo, los casos van de 0 a 5, mientras que en otro ejemplo van de 0 a 30, y ninguno de los ejemplos usa valores dispersos
Assylias
@assylias Hmm, interesante. Creo que entendí mal la pregunta. Déjame hacer un poco más de experimentación. Entonces, ¿estás diciendo que incluso con un rango contiguo de 0-30, el compilador usa un lookupswitch?
Vivin Paliath
@VivinPaliath: Sí, en mis pruebas las constantes de casos siempre son contiguas, por lo que básicamente estoy probando interruptores en [0, 1], [0, 1, 2], [0, 1, 2, 3] ... etc.
Andrew Bissell
@VivinPaliath No, el código de bytes siempre usa un conmutador de tabla; sin embargo, el compilador JIT no parece compilar el conmutador de tabla para ensamblarlo de la misma manera, dependiendo de cuántos elementos contenga.
Assylias
66
@VivinPaliath Podría haber formulado la pregunta con mayor claridad. Estoy un poco fuera de mi alcance cuando se trata de evaluar respuestas relacionadas con este código de bytes de bajo nivel y cosas de ensamblaje. Todavía me parece que la distinción de interruptor de tabla / interruptor de búsqueda es realmente importante aquí, y la suya es la única respuesta que emplea esos términos hasta ahora (aunque los otros probablemente exponen el mismo concepto con una terminología diferente). Además, también me gusta tener el enlace de especificaciones JVM.
Andrew Bissell
19

Como la pregunta ya está respondida (más o menos), aquí hay un consejo. Utilizar

private static final double[] mul={1d, 10d...};
static double multiplyByPowerOfTen(final double d, final int exponent) {
      if (exponent<0 || exponent>=mul.length) throw new ParseException();//or just leave the IOOBE be
      return mul[exponent]*d;
}

Ese código usa significativamente menos IC (caché de instrucciones) y siempre estará en línea. La matriz estará en la caché de datos L1 si el código está activo. La tabla de búsqueda es casi siempre una victoria. (especialmente en microbenchmarks: D)

Editar: si desea que el método esté en línea caliente, considere que las rutas no rápidas throw new ParseException()son tan cortas como mínimo o muévalas a un método estático separado (por lo tanto, haga que sean cortas como mínimo). Esa es throw new ParseException("Unhandled power of ten " + power, 0);una idea débil porque consume una gran parte del presupuesto de línea para el código que solo se puede interpretar: la concatenación de cadenas es bastante detallada en bytecode. Más información y un caso real con ArrayList

bestsss
fuente