Por que o Java alterna ints contíguos parece correr mais rápido com casos adicionados?


276

Estou trabalhando em algum código Java que precisa ser altamente otimizado, pois será executado em funções ativadas que são invocadas em muitos pontos da lógica do meu programa principal. Parte desse código envolve a multiplicação de doublevariáveis ​​por 10elevadas para int exponents não negativos arbitrários . Uma maneira rápida (edit: mas não o mais rápido possível, consulte Update 2 abaixo) para obter o valor multiplicado é switchsobre a 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);
   }
}

As elipses comentadas acima indicam que as case intconstantes continuam incrementando em 1, então existem realmente 19 cases no snippet de código acima. Desde que eu não tinha certeza se eu realmente precisa de todas as potências de 10, em casedeclarações 10através 18, eu corri alguns microbenchmarks comparando o tempo para completar 10 milhões de operações com essa switchdeclaração contra um switchcom apenas caseé 0através 9(com o exponentlimitado a 9 ou menos evite quebrar o aparado switch). Eu obtive o resultado bastante surpreendente (para mim, pelo menos!) De que quanto mais tempo switchcom mais caseinstruções realmente corria mais rápido.

Em uma cotovia, tentei adicionar ainda mais cases, que apenas retornavam valores fictícios, e descobri que era possível executar o switch ainda mais rápido com cerca de 22 a 27 cases declarados (mesmo que esses casos fictícios nunca sejam realmente atingidos enquanto o código está sendo executado). ) (Novamente, cases foram adicionados de maneira contígua, incrementando a caseconstante anterior em 1.) Essas diferenças no tempo de execução não são muito significativas: para uma aleatória exponententre 0e 10, a switchinstrução dummy padded termina 10 milhões de execuções em 1,49 segundos versus 1,54 segundos para os não preenchidos versão, para uma grande economia total de 5ns por execução. Portanto, não é o tipo de coisa que torna obsessivo o preenchimento de umswitchdeclaração vale o esforço do ponto de vista de otimização. Mas ainda acho curioso e contra-intuitivo que a switchnão se torne mais lento (ou, na melhor das hipóteses, mantenha o tempo O (1) constante ) para executar à medida que mais cases são adicionados a ela.

alternar resultados de benchmarking

Estes são os resultados que obtive ao executar com vários limites nos exponentvalores gerados aleatoriamente . Não incluí os resultados até 1o exponentlimite, mas a forma geral da curva permanece a mesma, com uma crista em torno da marca de 12 a 17 e um vale entre 18 e 28. Todos os testes foram executados no JUnitBenchmarks usando contêineres compartilhados para os valores aleatórios para garantir entradas de teste idênticas. Também executei os testes na ordem da switchdeclaração mais longa à mais curta e vice-versa, para tentar eliminar a possibilidade de problemas de teste relacionados ao pedido. Coloquei meu código de teste em um repositório do github se alguém quiser tentar reproduzir esses resultados.

Então, o que está acontecendo aqui? Alguns caprichos da minha arquitetura ou construção de micro-benchmark? Ou é o Java switchrealmente um pouco mais rápido para executar no 18a 28 casegama do que é de 11até 17?

repositório de teste do github "switch-experiment"

UPDATE: Limpei bastante a biblioteca de benchmarking e adicionei um arquivo de texto em / results com alguma saída em uma faixa mais ampla de exponentvalores possíveis . Também adicionei uma opção no código de teste para não lançar um Exceptionfrom default, mas isso não parece afetar os resultados.

ATUALIZAÇÃO 2: Encontrei uma discussão bastante boa sobre esse problema em 2009 no fórum xkcd aqui: http://forums.xkcd.com/viewtopic.php?f=11&t=33524 . A discussão do OP sobre o uso Array.binarySearch()me deu a ideia de uma implementação simples baseada em array do padrão de exponenciação acima. Não há necessidade de pesquisa binária, pois sei quais são as entradas no array. Parece funcionar três vezes mais rápido que o uso switch, obviamente à custa de parte do fluxo de controle que switchproporciona. Esse código também foi adicionado ao repositório do github.


64
Agora, todos os Googlers de todos os lugares terão precisamente 22 casos em todas as switchdeclarações, pois é claramente a solução mais ideal. : D (não mostre isso à minha liderança, por favor.) #
Asteri 25/03

2
Você tem um SSCCE mais simples? Este não compila para mim. Por mais fraco que eu esteja com o desempenho do Java, quero tentar.
Mysticial 25/03

5
Você pode achar útil a seção "Switches na JVM" na minha resposta sobre casos baseados em cadeias. Eu acho que o que está acontecendo aqui é que você está mudando de um lookupswitchpara um tableswitch. Desmontar seu código javapmostraria a você com certeza.
Erickson

2
Adicionei os jars de dependência à pasta / lib no repositório. @Mysticial Desculpe, eu meio que já passei muito tempo descendo este buraco de coelho! Se você retirar o "estende AbstractBenchmark" das classes de teste e se livrar das importações "com.carrotsearch", poderá executar apenas a dependência do JUnit, mas o material do carrotsearch é muito bom para filtrar parte do ruído do JIT e períodos de aquecimento. Infelizmente, não sei como executar esses testes JUnit fora do IntelliJ.
precisa

2
@AndrewBissell Consegui reproduzir seus resultados com uma referência muito mais simples. A ramificação versus tabela para o desempenho de tamanho pequeno versus médio foi um palpite bastante óbvio. Mas eu não tenho uma visão melhor do que ninguém sobre o mergulho indo para 30 casos ...
Mysticial

Respostas:


228

Conforme apontado pela outra resposta , como os valores de maiúsculas e minúsculas são contíguos (ao contrário de esparsos), o bytecode gerado para seus vários testes usa uma tabela de comutação (instrução bytecode tableswitch).

No entanto, uma vez que o JIT inicia seu trabalho e compila o bytecode em assembly, a tableswitchinstrução nem sempre resulta em uma matriz de ponteiros: às vezes a tabela de comutação é transformada no que parece ser lookupswitch(semelhante a uma estrutura if/ else if).

A descompilação do assembly gerado pelo JIT (ponto de acesso JDK 1.7) mostra que ele usa uma sucessão de if / else se quando houver 17 casos ou menos, uma matriz de ponteiros quando houver mais de 18 (mais eficiente).

A razão pela qual esse número mágico de 18 é usado parece estar no valor padrão do MinJumpTableSizesinalizador JVM (em torno da linha 352 no código).

Eu levantei o problema na lista do compilador de hotspot e parece ser um legado de testes anteriores . Observe que esse valor padrão foi removido no JDK 8 após a realização de mais testes de referência .

Finalmente, quando o método se torna muito longo (> 25 casos nos meus testes), ele não é mais incorporado com as configurações padrão da JVM - essa é a causa mais provável da queda no desempenho naquele momento.


Com 5 casos, o código descompilado fica assim (observe as instruções cmp / je / jg / jmp, o conjunto 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    

Com 18 casos, a montagem fica assim (observe a matriz de ponteiros que é usada e suprime a necessidade de todas as comparações: jmp QWORD PTR [r8+r10*1]salta diretamente para a multiplicação correta) - esse é o provável motivo da melhoria de desempenho:

[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    

E, finalmente, a montagem com 30 casos (abaixo) é semelhante a 18 casos, exceto pelo adicional movapd xmm0,xmm1que aparece no meio do código, conforme observado por @cHao - no entanto, o motivo mais provável para a queda no desempenho é que o método é muito por muito tempo para ser alinhado com as configurações padrão da 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    

7
@ syb0rg Para ser honesto eu não entendo os detalhes finos tanto ;-)
assylias

4
+1 para uma ótima resposta! Você poderia desmontar algo com mais de 30 casos para comparar quando o desempenho sair do "mergulho" no gráfico do OP?
asteri 25/03


2
@AndrewBissell Meu palpite é que o comportamento diferente é baseado em (i) testes de desempenho de arquitetura cruzada que mostraram que a matriz de ponteiros só é eficiente quando o número de casos é maior que 18 ou (ii) o código é perfilado como é executado e o criador de perfil determina qual abordagem é melhor durante o tempo de execução. Não consigo encontrar a resposta.
assylias 25/03

3
A desmontagem de 30 gabinetes e a de 18 gabinetes parecem praticamente iguais. As diferenças parecem limitadas principalmente a um pouco mais de embaralhamento de registros depois do 11º caso. Não posso dizer por que o JITter faz isso; parece desnecessário.
Chao

46

A caixa de comutação é mais rápida se os valores da caixa forem colocados em uma faixa estreita, por exemplo.

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

Porque, nesse caso, o compilador pode evitar a comparação de todos os trechos do caso na instrução switch. O compilador cria uma tabela de salto que contém endereços das ações a serem executadas em diferentes partes. O valor no qual a opção está sendo executada é manipulado para convertê-lo em um índice para jump table. Nesta implementação, o tempo gasto na instrução switch é muito menor que o tempo gasto em uma cascata equivalente da instrução if-else-if. Além disso, o tempo gasto na instrução switch é independente do número de pernas da caixa na instrução switch.

Conforme indicado na wikipedia sobre a instrução switch na seção Compilação.

Se o intervalo de valores de entrada é identificávelmente 'pequeno' e possui apenas algumas lacunas, alguns compiladores que incorporam um otimizador podem realmente implementar a instrução switch como uma tabela de ramificação ou uma matriz de indicadores de funções indexados, em vez de uma longa série de instruções condicionais. Isso permite que a instrução switch determine instantaneamente qual ramificação executar sem ter que passar por uma lista de comparações.


4
isso não está correto. Será mais rápido, independentemente dos valores dos casos serem estreitos ou amplos. É O (1) - não deve importar a diferença entre os valores dos casos.
Aniket Inge

6
@Aniket: Leia este artigo da wikipedia. pt.wikipedia.org/wiki/Branch_table
Vishal K

14
@ Aniket: Não é O (1) se a faixa é ampla e esparsa. Existem dois tipos de comutadores e, se o intervalo estiver muito espalhado, o Java o compilará em um "comutador de pesquisa" em vez de um "comutador de tabela". O primeiro requer uma comparação por ramo até ser encontrado, enquanto o segundo não.
Chao

4
A Wikipedia é um local decente para encontrar referências, mas não deve ser considerada uma fonte autorizada. Tudo o que você lê é, na melhor das hipóteses, informações de segunda mão.
Chao

6
@ Aniket: Com toda a justiça, a desmontagem é específica para uma determinada JVM em uma plataforma específica. Outros podem traduzir de maneira diferente. Alguns podem de fato usar uma tabela de hash para um interruptor de pesquisa. Ele ainda não funciona tão bem como um interruptor de mesa, mas pode pelo menos estar perto. Levaria mais tempo ao JIT e envolvia a aplicação de um algoritmo de hash na entrada. Portanto, embora o código de montagem resultante possa ser esclarecedor, também não é autoritário, a menos que você esteja falando especificamente sobre o Hotspot v1.7. qualquer que seja o Windows x86_64.
Chao

30

A resposta está no bytecode:

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");
        }
    }       
}

Bytecode correspondente; Apenas partes relevantes mostradas:

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");
        }
    }       
}

Bytecode correspondente; novamente, apenas as partes relevantes mostradas:

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 }

No primeiro caso, com intervalos estreitos, o bytecode compilado usa a tableswitch. No segundo caso, o bytecode compilado usa a lookupswitch.

Em tableswitch, o valor inteiro na parte superior da pilha é usado para indexar na tabela, para encontrar o alvo de ramificação / salto. Esse salto / ramificação é realizado imediatamente. Portanto, esta é uma O(1)operação.

A lookupswitché mais complicado. Nesse caso, o valor inteiro precisa ser comparado com todas as chaves da tabela até que a chave correta seja encontrada. Depois que a chave é encontrada, o alvo de ramificação / salto (para o qual essa chave é mapeada) é usado para o salto. A tabela usada lookupswitché classificada e um algoritmo de pesquisa binária pode ser usado para encontrar a chave correta. O desempenho para uma pesquisa binária é O(log n), e todo o processo também é O(log n), porque o salto ainda é O(1). Portanto, a razão pela qual o desempenho é mais baixo no caso de intervalos esparsos é que a chave correta deve ser procurada primeiro porque você não pode indexar diretamente na tabela.

Se houver valores esparsos e você apenas tiver tableswitchque usar, a tabela conterá essencialmente entradas falsas que apontam para a defaultopção Por exemplo, supondo que a última entrada SwitchTest10.javafoi em 21vez de 10, você obtém:

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 }

Portanto, o compilador basicamente cria essa enorme tabela contendo entradas falsas entre as lacunas, apontando para o alvo da ramificação da defaultinstrução. Mesmo se não houver um default, ele conterá entradas apontando para a instrução após o bloco do comutador. Fiz alguns testes básicos e descobri que, se a diferença entre o último índice e o anterior ( 9) for maior que 35, ele usa a em lookupswitchvez de a tableswitch.

O comportamento da switchinstrução é definido em Java Virtual Machine Specification (§3.10) :

Onde os casos do comutador são escassos, a representação da tabela da instrução do comutador de tabelas se torna ineficiente em termos de espaço. A instrução lookupswitch pode ser usada em seu lugar. A instrução lookupswitch emparelha chaves int (os valores dos rótulos de maiúsculas e minúsculas) com deslocamentos de destino em uma tabela. Quando uma instrução de pesquisa é executada, o valor da expressão do comutador é comparado com as chaves na tabela. Se uma das chaves corresponder ao valor da expressão, a execução continuará no deslocamento de destino associado. Se nenhuma chave corresponder, a execução continuará no destino padrão. [...]


1
Eu entendi pela pergunta que os números são sempre contíguos, mas o intervalo é mais ou menos longo - ou seja, em um exemplo, os casos vão de 0 a 5, enquanto em outro exemplo, eles vão de 0 a 30 - e nenhum dos exemplos usa valores esparsos
assylias 25/03

@assylias Hmm, interessante. Acho que não entendi a pergunta. Deixe-me fazer mais algumas experiências. Então você está dizendo que, mesmo com um intervalo contíguo de 0 a 30, o compilador usa a lookupswitch?
Vivin Paliath 25/03

@VivinPaliath: Sim, nos meus testes as constantes do caso são sempre contíguas, então estou basicamente testando os interruptores em [0, 1], [0, 1, 2], [0, 1, 2, 3] ... etc
Andrew Bissell

@VivinPaliath Não, o bytecode sempre usa um comutador de tabelas - no entanto, o compilador JIT parece não compilar o comutador de tabelas para montagem da mesma maneira, dependendo de quantos itens ele contém.
assylias 25/03

6
@VivinPaliath Eu poderia ter formulado a pergunta mais claramente, com certeza. Estou meio fora de questão quando se trata de avaliar respostas que envolvem esse código de byte de baixo nível e material de montagem. Ainda me parece que a distinção entre comutador de tabela / pesquisa é realmente importante aqui, e a sua é a única resposta que emprega esses termos até agora (embora os outros provavelmente estejam estabelecendo o mesmo conceito com terminologia diferente). Além disso, eu também gosto de ter o link JVM Spec.
Andrew Bissell 25/03

19

Como a pergunta já foi respondida (mais ou menos), aqui está uma dica. Usar

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;
}

Esse código usa significativamente menos IC (cache de instruções) e será sempre incorporado. A matriz estará no cache de dados L1 se o código estiver quente. A tabela de pesquisa quase sempre é uma vitória. (especialmente nas marcas de micro-marcas: D)

Editar: se você deseja que o método seja hot-inline, considere os caminhos não rápidos como o throw new ParseException()mais curto possível ou mova-os para separar o método estático (tornando-o mais curto). Essa é throw new ParseException("Unhandled power of ten " + power, 0);uma idéia fraca, pois ela consome muito do orçamento interno do código que pode ser interpretado - a concatenação de strings é bastante detalhada no bytecode. Mais informações e um caso real com ArrayList

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.