Perché Java attiva gli input contigui sembra funzionare più velocemente con i casi aggiunti?


276

Sto lavorando su alcuni codici Java che devono essere altamente ottimizzati in quanto verranno eseguiti in funzioni attive che vengono invocate in molti punti della mia logica principale del programma. Parte di questo codice comporta la moltiplicazione di doublevariabili per 10sorti a int exponents arbitrari non negativi . Un modo rapido (modifica: ma non il più veloce possibile, vedere l'aggiornamento 2 di seguito) per ottenere il valore moltiplicato è switchquello di 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);
   }
}

Le ellissi commentate sopra indicano che le case intcostanti continuano ad aumentare di 1, quindi ci sono davvero 19 casesecondi nello snippet di codice sopra. Dal momento che non ero sicuro se avrei effettivamente avuto bisogno di tutti i poteri di 10 in casedichiarazioni 10attraverso 18, ho eseguito alcuni microbenchmark confrontando il tempo per completare 10 milioni di operazioni con questa switchaffermazione rispetto a switchcon solo cases0 thru 9(con il exponentlimite di 9 o meno a evitare di abbattere il taglio switch). Ho ottenuto il risultato piuttosto sorprendente (almeno per me!) Che più switcha lungo con più caseaffermazioni è effettivamente corso più velocemente.

In seguito, ho provato ad aggiungere ancora più messaggi caseche hanno appena restituito valori fittizi e ho scoperto che avrei potuto far funzionare lo switch ancora più velocemente con circa 22-27 dichiarati case(anche se quei casi fittizi non vengono mai colpiti mentre il codice è in esecuzione ). (Ancora una volta, casesono state aggiunte s in modo contiguo aumentando l' affermazione precedente che vale la pena da un punto di vista dell'ottimizzazione. Ma trovo ancora curioso e controintuitivo che a non diventi più lento (o forse nella migliore delle ipotesi mantenga costante O ( 1) tempo) per eseguire come di piùcase costante di 1.) Queste differenze nei tempi di esecuzione non sono molto significative: per casuale exponenttra 0e vengono aggiunti s. 10 , l' switchistruzione fittizia imbottita termina 10 milioni di esecuzioni in 1,49 secondi contro 1,54 secondi per i non imbottiti versione, per un risparmio complessivo di 5 ns per esecuzione. Quindi, non il tipo di cosa che rende ossessionante il riempimento aswitchswitchcase

cambiare i risultati del benchmarking

Questi sono i risultati che ho ottenuto correndo con vari limiti sui exponentvalori generati casualmente . Non ho incluso fino in fondo i risultati1 per il exponentlimite, ma la forma generale della curva rimane la stessa, con una cresta intorno al segno del caso 12-17 e una valle tra 18-28. Tutti i test sono stati eseguiti in JUnitBenchmarks utilizzando contenitori condivisi per i valori casuali per garantire input di test identici. Ho anche eseguito i test in ordine sia dall'istruzione più lunga switchalla più breve, sia viceversa, per cercare di eliminare la possibilità di problemi relativi all'ordine. Ho messo il mio codice di test su un repository github se qualcuno vuole provare a riprodurre questi risultati.

Allora, cosa sta succedendo qui? Alcuni capricci della mia architettura o costruzione di micro-benchmark? O è il Java switchin realtà un po 'più veloce da eseguire nel 18per 28 casegamma di quello che è da11 fino a 17?

repository test github "interruttore-esperimento"

AGGIORNAMENTO: ho ripulito un po 'la libreria di benchmarking e ho aggiunto un file di testo in / results con un po' di output su una gamma più ampia di exponentvalori possibili . Ho anche aggiunto un'opzione nel codice di test per non lanciare unException da default, ma questo non sembra influenzare i risultati.

AGGIORNAMENTO 2: ho trovato alcune discussioni abbastanza valide su questo problema nel 2009 sul forum di xkcd qui: http://forums.xkcd.com/viewtopic.php?f=11&t=33524 . La discussione dell'OP sull'uso Array.binarySearch()mi ha dato l'idea di una semplice implementazione basata su array del modello di esponenziale sopra. Non è necessario per la ricerca binaria poiché so quali sono le voci in array. Sembra funzionare circa 3 volte più veloce dell'uso switch, ovviamente a scapito di parte del flusso di controllo che switchoffre. Quel codice è stato aggiunto anche al repository github.


64
Ora tutti i googler di tutto il mondo avranno esattamente 22 casi in tutte le switchdichiarazioni, poiché è chiaramente la soluzione più ottimale. : D (Non mostrarlo al mio comando, per favore.)
asteri

2
Hai un SSCCE più semplice? Questo non si compila per me. Per quanto debole sia con le prestazioni di Java, voglio fare un tentativo.
Mistico

5
Potresti trovare utile la sezione "Switches in the JVM" nella mia risposta sui casi basati su stringhe. Penso che ciò che sta accadendo qui sia che stai passando da lookupswitcha a tableswitch. Disassemblare il codice con javaplo mostrerebbe di sicuro.
Erickson,

2
Ho aggiunto i vasetti di dipendenza alla cartella / lib nel repository. @Mysticial Spiacente, ho già passato troppo tempo a scendere in questa tana del coniglio! Se togli le "estensioni di AbstractBenchmark" dalle classi di test e ti sbarazzi delle importazioni di "com.carrotsearch", puoi eseguire solo la dipendenza JUnit, ma la roba di carrotsearch è piuttosto bella per filtrare parte del rumore proveniente dalla JIT e periodi di riscaldamento. Sfortunatamente non so come eseguire questi test JUnit al di fuori di IntelliJ.
Andrew Bissell,

2
@AndrewBissell Sono riuscito a riproporre i tuoi risultati con un benchmark molto più semplice. Il ramo contro la tabella per le prestazioni di dimensioni piccole contro medie era un'ipotesi piuttosto ovvia. Ma non ho una visione migliore di chiunque altro sulla caduta in 30 casi ...
Mistico

Risposte:


228

Come sottolineato dall'altra risposta , poiché i valori del caso sono contigui (al contrario di sparsi), il bytecode generato per i vari test utilizza una tabella di switch (istruzione bytecode tableswitch).

Tuttavia, una volta che JIT inizia il suo lavoro e compila il bytecode in assembly, l' tableswitchistruzione non risulta sempre in una matrice di puntatori: a volte la tabella degli switch viene trasformata in ciò che sembra un lookupswitch(simile a una if/ else ifstruttura).

La decompilazione dell'assembly generato da JIT (hotspot JDK 1.7) mostra che utilizza una successione di if / else se in presenza di 17 casi o meno, un array di puntatori quando ce ne sono più di 18 (più efficienti).

Il motivo per cui viene usato questo numero magico di 18 sembra scendere al valore predefinito del MinJumpTableSizeflag JVM (intorno alla riga 352 nel codice).

Ho sollevato il problema nell'elenco dei compilatori di hotspot e sembra essere un'eredità di test passati . Si noti che questo valore predefinito è stato rimosso in JDK 8 dopo l' esecuzione di ulteriori benchmark .

Infine, quando il metodo diventa troppo lungo (> 25 casi nei miei test), non è più in linea con le impostazioni JVM predefinite - questa è la causa più probabile del calo delle prestazioni a quel punto.


Con 5 casi, il codice decompilato è simile al seguente (notare le istruzioni cmp / je / jg / jmp, l'assemblaggio per 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 casi, l'assemblaggio si presenta così (notare la matrice di puntatori che viene utilizzata e sopprime la necessità di tutti i confronti: jmp QWORD PTR [r8+r10*1]passa direttamente alla giusta moltiplicazione) - questa è la probabile ragione del miglioramento delle prestazioni:

[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 infine l'assemblaggio con 30 casi (sotto) sembra simile a 18 casi, ad eccezione dell'ulterioremovapd xmm0,xmm1 che appare verso la metà del codice, come notato da @cHao - tuttavia il motivo più probabile per il calo delle prestazioni è che il metodo è troppo Lungamente da includere nelle impostazioni JVM predefinite:

[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 Ad essere sincero, non capisco neanche i dettagli ;-)
assylias

4
+1 per un'ottima risposta! Potresti disassemblare qualcosa con oltre 30 casi da confrontare quando la performance esce dal "calo" nel grafico del PO?
asteri,


2
@AndrewBissell La mia ipotesi è che il diverso comportamento si basi su uno dei due (i) test delle prestazioni tra architetture che hanno dimostrato che l'array di puntatori è efficiente solo quando il numero di casi è maggiore di 18 o (ii) il codice è profilato come viene eseguito e il profiler determina quale approccio è migliore durante il runtime. Non riesco a trovare la risposta.
Assylias,

3
Lo smontaggio di 30 casi e quello di 18 casi sembrano quasi uguali. Le differenze sembrano per lo più limitate a un ulteriore po 'di riordino del registro dopo l'11 ° caso. Non riesco a dire perché JITter lo faccia; sembra superfluo.
cHao,

46

Switch - case è più veloce se i valori del case sono collocati in un intervallo ristretto Ad es.

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

Perché, in questo caso, il compilatore può evitare di eseguire un confronto per ogni segmento del caso nell'istruzione switch. Il compilatore crea una tabella di salto che contiene gli indirizzi delle azioni da eseguire su diverse gambe. Il valore su cui viene eseguito lo switch viene manipolato per convertirlo in un indice injump table . In questa implementazione, il tempo impiegato nell'istruzione switch è molto inferiore al tempo impiegato in un'istruzione if-else-if equivalente a cascata. Inoltre, il tempo impiegato nell'istruzione switch è indipendente dal numero di segmenti del case nell'istruzione switch.

Come affermato in wikipedia sull'istruzione switch nella sezione Compilation.

Se l'intervallo di valori di input è identificabilmente "piccolo" e presenta solo alcune lacune, alcuni compilatori che incorporano un ottimizzatore potrebbero effettivamente implementare l'istruzione switch come una tabella di diramazione o un array di puntatori di funzione indicizzati anziché una lunga serie di istruzioni condizionali. Ciò consente all'istruzione switch di determinare istantaneamente quale ramo eseguire senza dover passare attraverso un elenco di confronti.


4
non è corretto. Sarà più veloce a prescindere dal fatto che i valori del case siano ristretti o ampi nel range. È O (1) - non dovrebbe importare quanto siano separati i valori del caso.
Aniket Inge,

6
@Aniket: leggi questo articolo di Wikipedia. en.wikipedia.org/wiki/Branch_table
Vishal K

14
@Aniket: non è O (1) se l'intervallo è ampio e scarso. Esistono due tipi di switch e, se l'intervallo è troppo esteso, Java lo compilerà in un "interruttore di ricerca" anziché in un "interruttore di tabella". Il primo richiede un confronto per ramo fino a quando non viene trovato, mentre il secondo no.
cHao,

4
Wikipedia è un posto decente per trovare riferimenti, ma non dovrebbe essere considerata una fonte autorevole. Tutto ciò che leggi contiene al meglio informazioni di seconda mano.
cHao,

6
@Aniket: In tutta onestà, lo smontaggio è specifico per una determinata JVM su una piattaforma specifica. Altri possono tradurlo in modo diverso. Alcuni potrebbero infatti utilizzare una tabella hash per un interruttore di ricerca. Non funzionerà ancora come un interruttore da tavolo, ma potrebbe almeno essere vicino. Ci vorrebbe solo più tempo per JIT, e implicherebbe l'applicazione di un algoritmo di hash all'input. Quindi, sebbene il codice assembly risultante possa essere illuminante, non è neanche autorevole a meno che tu non stia parlando specificamente di Hotspot v1.7.whatever su Windows x86_64.
cHao,

30

La risposta sta nel 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 corrispondente; mostrate solo le parti rilevanti:

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 corrispondente; di nuovo, sono mostrate solo le parti rilevanti:

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 }

Nel primo caso, con intervalli ristretti, il bytecode compilato utilizza a tableswitch. Nel secondo caso, il bytecode compilato utilizza a lookupswitch.

In tableswitch, il valore intero nella parte superiore dello stack viene utilizzato per indicizzare nella tabella, per trovare la destinazione branch / jump. Questo salto / ramo viene quindi eseguito immediatamente. Quindi, questa è O(1)un'operazione.

A lookupswitchè più complicato. In questo caso, il valore intero deve essere confrontato con tutte le chiavi della tabella fino a quando non viene trovata la chiave corretta. Dopo aver trovato la chiave, viene utilizzato il target branch / jump (su cui è mappata questa chiave) per il salto. La tabella utilizzata lookupswitchviene ordinata e un algoritmo di ricerca binaria può essere utilizzato per trovare la chiave corretta. Le prestazioni per una ricerca binaria sono O(log n)e anche l'intero processo O(log n), perché il salto è fermo O(1). Quindi il motivo per cui le prestazioni sono inferiori nel caso di intervalli sparsi è che la chiave corretta deve prima essere cercata perché non è possibile indicizzare direttamente nella tabella.

Se ci sono valori sparsi e hai solo un tableswitchda usare, la tabella conterrebbe essenzialmente voci fittizie che puntano defaultall'opzione. Ad esempio, supponendo che l'ultima voce sia SwitchTest10.javastata 21invece di 10, ottieni:

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 }

Quindi il compilatore fondamentalmente crea questa enorme tabella contenente voci fittizie tra gli spazi vuoti, che punta al target branch defaultdell'istruzione. Anche se non è presente default, conterrà voci che puntano all'istruzione dopo il blocco switch. Ho fatto alcuni test di base e ho scoperto che se il divario tra l'ultimo indice e il precedente ( 9) è maggiore di 35, utilizza un lookupswitchanziché un tableswitch.

Il comportamento dell'istruzioneswitch è definito nelle Specifiche della macchina virtuale Java (§3.10) :

Laddove i casi dell'interruttore sono sparsi, la rappresentazione della tabella delle istruzioni dello switch di tabella diventa inefficiente in termini di spazio. In alternativa, è possibile utilizzare l'istruzione lookupswitch. L'istruzione del commutatore di ricerca accoppia i tasti int (i valori delle etichette del caso) con gli offset di destinazione in una tabella. Quando viene eseguita un'istruzione lookupswitch, il valore dell'espressione dello switch viene confrontato con i tasti nella tabella. Se una delle chiavi corrisponde al valore dell'espressione, l'esecuzione continua all'offset di destinazione associato. Se nessuna chiave corrisponde, l'esecuzione continua sulla destinazione predefinita. [...]


1
Ho capito dalla domanda che i numeri sono sempre contigui ma l'intervallo è più o meno lungo - vale a dire in un esempio i casi vanno da 0 a 5 mentre in un altro esempio vanno da 0 a 30 - e nessuno degli esempi usa valori sparsi
Assylias,

@assylias Hmm, interessante. Immagino di aver frainteso la domanda. Lasciami fare qualche altra sperimentazione. Quindi stai dicendo che anche con un intervallo contiguo da 0 a 30, il compilatore usa un lookupswitch?
Vivin Paliath,

@VivinPaliath: Sì, nei miei test le costanti del caso sono sempre contigue, quindi sostanzialmente sto testando gli switch su [0, 1], [0, 1, 2], [0, 1, 2, 3] ... ecc
Andrew Bissell,

@VivinPaliath No, il bytecode utilizza sempre un interruttore da tavolo, tuttavia il compilatore JIT non sembra compilare l'interruttore da tavolo per l'assemblaggio allo stesso modo, a seconda del numero di elementi che contiene.
assylias

6
@VivinPaliath Avrei potuto formulare la domanda in modo più chiaro di sicuro. Sono un po 'fuori dalla mia profondità quando si tratta di valutare le risposte che coinvolgono questo bytecode di basso livello e roba di assemblaggio. Mi sembra ancora che la distinzione switchwitch / lookups sia effettivamente importante qui, e la tua è l'unica risposta che impiega quei termini finora (anche se gli altri probabilmente stanno esponendo lo stesso concetto con una terminologia diversa). Inoltre mi piace avere anche il collegamento alle specifiche JVM.
Andrew Bissell

19

Poiché alla domanda è già stata data una risposta (più o meno), ecco alcuni suggerimenti. Uso

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

Tale codice utilizza significativamente meno IC (cache delle istruzioni) e sarà sempre integrato. L'array sarà nella cache di dati L1 se il codice è attivo. La tabella di ricerca è quasi sempre una vittoria. (in particolare sui microbenchmark: D)

Modifica: se desideri che il metodo sia in linea di principio, considera i percorsi non veloci come throw new ParseException() come brevi come minimo o spostarli in un metodo statico separato (rendendoli quindi brevi come minimo). Questa è throw new ParseException("Unhandled power of ten " + power, 0);un'idea debole b / c che consuma molto del budget interno per il codice che può essere appena interpretato - la concatenazione di stringhe è abbastanza dettagliata nel bytecode. Maggiori informazioni e un caso reale con ArrayList

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.