Mengapa Java mengaktifkan ints yang berdekatan terlihat berjalan lebih cepat dengan case yang ditambahkan?

276

Saya sedang mengerjakan beberapa kode Java yang perlu sangat dioptimalkan karena akan berjalan di fungsi panas yang dipanggil di banyak titik dalam logika program utama saya. Bagian dari kode ini melibatkan mengalikan doublevariabel dengan 10menaikkan ke non-negatif int exponents sembarang . Salah satu cara cepat (edit: tapi bukan yang tercepat mungkin, lihat Update 2 di bawah) untuk mendapatkan nilai dikalikan adalah untuk switchpada 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);
   }
}

Elips yang dikomentari di atas menunjukkan bahwa case intkonstanta terus bertambah 1, sehingga benar-benar ada 19 casedetik dalam cuplikan kode di atas. Karena saya tidak yakin apakah saya akan benar-benar membutuhkan semua kekuatan 10 dalam casepernyataan 10melalui 18, saya menjalankan beberapa microbenchmark membandingkan waktu untuk menyelesaikan 10 juta operasi dengan switchpernyataan ini versus switchdengan hanya cases 0melalui 9(dengan batas exponent9 atau kurang untuk hindari melanggar pared-down switch). Saya mendapatkan hasil yang agak mengejutkan (bagi saya, setidaknya!) Bahwa semakin lama switchdengan lebih banyak casepernyataan benar-benar berjalan lebih cepat.

Pada lark, saya mencoba menambahkan lebih banyak lagi caseyang baru saja mengembalikan nilai-nilai dummy, dan menemukan bahwa saya bisa beralih untuk berjalan lebih cepat dengan sekitar 22-27 yang dinyatakan cases (walaupun kotak-kotak boneka itu tidak pernah benar-benar mengenai saat kode sedang berjalan ). (Sekali lagi, caseditambahkan dengan cara yang berdekatan dengan menambah casekonstanta sebelumnya dengan 1). Perbedaan waktu eksekusi ini tidak terlalu signifikan: untuk acak exponentantara 0dan 10, switchpernyataan empuk selesai menyelesaikan 10 juta eksekusi dalam 1,49 detik dibandingkan 1,54 detik untuk yang tidak diadili versi, untuk penghematan total 5ns per eksekusi. Jadi, bukan hal yang membuat terobsesi padding out aswitchpernyataan sepadan dengan usaha dari sudut pandang optimasi. Tapi saya masih merasa penasaran dan kontra-intuitif bahwa switchtidak menjadi lebih lambat (atau mungkin lebih baik mempertahankan waktu O (1) konstan ) untuk dieksekusi karena lebih casebanyak ditambahkan ke dalamnya.

beralih hasil tolok ukur

Ini adalah hasil yang saya peroleh dari menjalankan dengan berbagai batasan pada exponentnilai yang dihasilkan secara acak . Saya tidak termasuk hasil semua jalan ke 1untuk exponentbatas, namun bentuk umum dari kurva tetap sama, dengan punggung bukit sekitar tanda 12-17 kasus, dan lembah antara 18-28. Semua tes dijalankan di JUnitBenchmarks menggunakan wadah bersama untuk nilai acak untuk memastikan input pengujian yang identik. Saya juga menjalankan tes baik dalam urutan dari switchpernyataan terpanjang ke terpendek, dan sebaliknya, untuk mencoba dan menghilangkan kemungkinan masalah tes terkait pemesanan. Saya telah meletakkan kode pengujian saya di repo github jika ada yang ingin mencoba mereproduksi hasil ini.

Jadi, apa yang terjadi di sini? Beberapa keanehan arsitektur saya atau konstruksi patok mikro? Atau adalah Java switchbenar-benar sedikit lebih cepat untuk mengeksekusi dalam 18untuk 28 caserentang daripada dari 11atas ke 17?

repo uji github "beralih-percobaan"

UPDATE: Saya membersihkan perpustakaan benchmarking sedikit dan menambahkan file teks di / hasil dengan beberapa output di kisaran yang lebih luas dari exponentnilai yang mungkin . Saya juga menambahkan opsi dalam kode pengujian untuk tidak membuang Exceptiondari default, tetapi ini tampaknya tidak mempengaruhi hasil.

UPDATE 2: Menemukan beberapa diskusi yang cukup bagus tentang masalah ini dari tahun 2009 di forum xkcd di sini: http://forums.xkcd.com/viewtopic.php?f=11&t=33524 . Diskusi OP tentang penggunaan Array.binarySearch()memberi saya ide untuk implementasi berbasis pola array sederhana dari pola eksponensial di atas. Tidak perlu mencari biner karena saya tahu apa entri dalam array. Tampaknya berjalan sekitar 3 kali lebih cepat daripada menggunakan switch, jelas dengan mengorbankan beberapa aliran kontrol yang diberikan switch. Kode itu telah ditambahkan ke repo github juga.

Andrew Bissell
sumber
64
Sekarang semua Googler di mana-mana akan memiliki tepat 22 kasus dalam semua switchpernyataan, karena itu jelas solusi yang paling optimal. : D (Tolong, jangan tunjukkan ini pada petunjuk saya.)
asteri
2
Apakah Anda memiliki SSCCE yang lebih sederhana? Yang ini tidak cocok untuk saya. Selemah saya dengan kinerja Java, saya ingin mencoba ini.
Mysticial
5
Anda mungkin menemukan bagian "Beralih di JVM" dalam jawaban saya tentang kasus berbasis string bermanfaat. Saya pikir apa yang terjadi di sini adalah bahwa Anda beralih dari a lookupswitchke a tableswitch. Membongkar kode Anda javapakan menunjukkan kepada Anda dengan pasti.
erickson
2
Saya menambahkan stoples ketergantungan ke folder / lib di repo. @Mysticial Maaf, saya sudah menghabiskan terlalu banyak waktu di lubang kelinci ini! Jika Anda mengambil "extends AbstractBenchmark" dari kelas tes dan menyingkirkan impor "com.carrotsearch", Anda dapat menjalankannya dengan hanya ketergantungan JUnit, tetapi item carrotsearch cukup bagus untuk menyaring beberapa suara dari JIT dan periode pemanasan. Sayangnya saya tidak tahu bagaimana menjalankan tes JUnit ini di luar IntelliJ.
Andrew Bissell
2
@AndrewBissell Saya berhasil mengecam hasil Anda dengan tolok ukur yang lebih sederhana. Cabang vs tabel untuk kinerja ukuran kecil vs menengah adalah dugaan yang agak jelas. Tetapi saya tidak memiliki wawasan yang lebih baik daripada orang lain tentang penurunan yang terjadi pada 30 kasus ...
Mysticial

Jawaban:

228

Seperti yang ditunjukkan oleh jawaban yang lain , karena nilai case bersebelahan (bukan sparse), bytecode yang dihasilkan untuk berbagai tes Anda menggunakan tabel switch (instruksi bytecode tableswitch).

Namun, begitu JIT memulai tugasnya dan mengkompilasi bytecode ke dalam assembly, tableswitchinstruksi tidak selalu menghasilkan array pointer: kadang-kadang tabel switch ditransformasikan menjadi apa yang tampak seperti lookupswitch(mirip dengan struktur if/ else if).

Mengurai kumpulan yang dihasilkan oleh JIT (hotspot JDK 1.7) menunjukkan bahwa ia menggunakan suksesi if / else jika ketika ada 17 kasus atau kurang, sebuah array pointer ketika ada lebih dari 18 (lebih efisien).

Alasan mengapa angka ajaib 18 ini digunakan tampaknya turun ke nilai default dari MinJumpTableSizebendera JVM (sekitar baris 352 dalam kode).

Saya telah mengangkat masalah ini pada daftar kompiler hotspot dan sepertinya merupakan warisan dari pengujian sebelumnya . Perhatikan bahwa nilai default ini telah dihapus di JDK 8 setelah lebih banyak pembandingan dilakukan .

Akhirnya, ketika metode ini menjadi terlalu lama (> 25 kasus dalam pengujian saya), itu tidak diuraikan lagi dengan pengaturan JVM default - yang merupakan penyebab kemungkinan penurunan kinerja pada saat itu.


Dengan 5 case, kode yang didekompilasi terlihat seperti ini (perhatikan instruksi cmp / je / jg / jmp, assembly untuk 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    

Dengan 18 kasing, susunannya terlihat seperti ini (perhatikan susunan pointer yang digunakan dan menekan kebutuhan untuk semua perbandingan: jmp QWORD PTR [r8+r10*1]melompat langsung ke perkalian yang benar) - itulah kemungkinan alasan peningkatan kinerja:

[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    

Dan akhirnya perakitan dengan 30 kasus (di bawah) terlihat mirip dengan 18 kasus, kecuali untuk tambahan movapd xmm0,xmm1yang muncul di bagian tengah kode, seperti yang terlihat oleh @cHao - namun alasan paling mungkin untuk penurunan kinerja adalah bahwa metode ini terlalu lama untuk disejajarkan dengan pengaturan JVM default:

[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    
assylias
sumber
7
@ syb0rg Sejujurnya aku juga tidak mengerti detailnya ;-)
assylias
4
+1 untuk jawaban yang bagus! Bisakah Anda membongkar sesuatu dengan 30+ kotak untuk dibandingkan ketika kinerja keluar dari "dip" di grafik OP?
asteri
4
@VivinPaliath stackoverflow.com/questions/1503479/…
assylias
2
@AndrewBissell Dugaan saya adalah bahwa perilaku yang berbeda didasarkan pada (i) tes kinerja lintas-arsitektur yang telah menunjukkan bahwa array pointer hanya efisien ketika jumlah kasus lebih besar dari 18 atau (ii) kode diprofilkan sebagai itu dijalankan dan profiler menentukan pendekatan mana yang lebih baik selama runtime. Saya tidak dapat menemukan jawabannya.
assylias
3
30 kasus pembongkaran dan 18 kasus sebagian besar terlihat sama. Perbedaan tampaknya sebagian besar terbatas pada sedikit ekstra pengocokan register setelah sekitar kasus ke-11. Tidak bisa mengatakan mengapa JITter melakukan itu; tampaknya tidak perlu.
cHao
46

Switch - case lebih cepat jika nilai case ditempatkan dalam range sempit Eg.

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

Karena, dalam hal ini kompiler dapat menghindari melakukan perbandingan untuk setiap kasus dalam pernyataan switch. Kompiler membuat tabel lompatan yang berisi alamat tindakan yang akan diambil pada kaki yang berbeda. Nilai di mana saklar sedang dilakukan dimanipulasi untuk mengubahnya menjadi indeks ke jump table. Dalam implementasi ini, waktu yang diambil dalam pernyataan switch jauh lebih sedikit daripada waktu yang diambil dalam kaskade pernyataan if-else-if yang setara. Juga waktu yang diambil dalam pernyataan sakelar tidak tergantung dari jumlah kaki sakelar dalam pernyataan sakelar.

Seperti yang dinyatakan dalam wikipedia tentang pernyataan beralih di bagian Kompilasi.

Jika rentang nilai input dapat dikenali 'kecil' dan hanya memiliki beberapa celah, beberapa kompiler yang memasukkan pengoptimal mungkin benar-benar mengimplementasikan pernyataan switch sebagai tabel cabang atau array dari pointer fungsi yang diindeks alih-alih serangkaian instruksi kondisional yang panjang. Ini memungkinkan pernyataan peralihan untuk menentukan secara instan cabang mana yang akan dieksekusi tanpa harus melalui daftar perbandingan.

Vishal K
sumber
4
itu tidak benar. Ini akan lebih cepat terlepas dari nilai case yang sempit atau lebar dalam jangkauan. Ini adalah O (1) - seharusnya tidak masalah seberapa jauh nilai case.
Aniket Inge
6
@Aniket: Baca artikel wikipedia ini. en.wikipedia.org/wiki/Branch_table
Vishal K
14
@Aniket: Bukan O (1) jika jangkauannya lebar dan jarang. Ada dua jenis switch, dan jika jangkauannya terlalu tersebar, Java akan mengkompilasinya menjadi "lookupswitch" daripada "tableswitch". Yang pertama membutuhkan perbandingan per cabang sampai yang ditemukan, sedangkan yang kedua tidak.
cHao
4
Wikipedia adalah tempat yang layak untuk mencari referensi, tetapi jangan dianggap sebagai sumber yang berwibawa. Apa pun yang Anda baca di sana adalah informasi bekas terbaik.
cHao
6
@Aniket: Dalam semua keadilan, pembongkaran khusus untuk JVM yang diberikan pada platform tertentu. Orang lain dapat menerjemahkannya secara berbeda. Beberapa mungkin sebenarnya menggunakan tabel hash untuk lookupswitch. Itu masih tidak akan berkinerja sebaik tableswitch, tapi setidaknya bisa dekat. Ini akan memakan waktu lebih lama untuk JIT, dan melibatkan penerapan algoritma hashing pada input. Jadi, meskipun kode perakitan yang dihasilkan dapat mencerahkan, itu juga tidak berwibawa kecuali jika Anda secara khusus berbicara tentang Hotspot v1.7. Apa pun di Windows x86_64.
cHao
30

Jawabannya terletak pada 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 yang sesuai; hanya bagian yang relevan yang ditampilkan:

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 yang sesuai; lagi, hanya bagian yang relevan yang ditampilkan:

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 }

Dalam kasus pertama, dengan rentang yang sempit, bytecode yang dikompilasi menggunakan a tableswitch. Dalam kasus kedua, bytecode yang dikompilasi menggunakan a lookupswitch.

Dalam tableswitch, nilai integer di bagian atas tumpukan digunakan untuk mengindeks ke dalam tabel, untuk menemukan target cabang / lompat. Lompatan / cabang ini kemudian dilakukan segera. Karenanya, ini adalah O(1)operasi.

A lookupswitchlebih rumit. Dalam hal ini, nilai integer perlu dibandingkan dengan semua kunci dalam tabel sampai kunci yang benar ditemukan. Setelah kunci ditemukan, target cabang / lompat (bahwa kunci ini dipetakan ke) digunakan untuk lompat. Tabel yang digunakan lookupswitchdiurutkan dan algoritma pencarian biner dapat digunakan untuk menemukan kunci yang benar. Performa untuk pencarian biner adalah O(log n), dan seluruh proses juga O(log n), karena lompatan masih O(1). Jadi alasan kinerjanya lebih rendah dalam hal rentang jarang adalah bahwa kunci yang benar pertama-tama harus dilihat karena Anda tidak dapat mengindeks ke dalam tabel secara langsung.

Jika ada nilai jarang dan Anda hanya tableswitchperlu menggunakan, tabel pada dasarnya akan berisi entri dummy yang mengarah ke defaultopsi. Misalnya, dengan asumsi bahwa entri terakhir dalam SwitchTest10.javaadalah 21bukan 10, Anda mendapatkan:

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 }

Jadi kompiler pada dasarnya membuat tabel besar ini yang berisi entri boneka di antara celah, menunjuk ke target cabang defaultinstruksi. Bahkan jika tidak ada default, itu akan berisi entri yang menunjuk ke instruksi setelah blok switch. Saya melakukan beberapa tes dasar, dan saya menemukan bahwa jika kesenjangan antara indeks terakhir dan yang sebelumnya ( 9) lebih besar daripada 35, ia menggunakan lookupswitchalih - alih a tableswitch.

Perilaku switchpernyataan didefinisikan dalam Java Virtual Machine Specification (§3.10) :

Jika case switch jarang, representasi tabel dari instruksi tableswitch menjadi tidak efisien dalam hal ruang. Instruksi lookupswitch dapat digunakan sebagai gantinya. Instruksi lookupswitch memasangkan kunci int (nilai-nilai label kasus) dengan offset target dalam sebuah tabel. Ketika instruksi lookupswitch dieksekusi, nilai ekspresi switch dibandingkan dengan kunci dalam tabel. Jika salah satu kunci cocok dengan nilai ekspresi, eksekusi berlanjut pada target offset yang terkait. Jika tidak ada kunci yang cocok, eksekusi berlanjut pada target default. [...]

Vivin Paliath
sumber
1
Saya mengerti dari pertanyaan bahwa angka-angka selalu berdampingan tetapi kisarannya kurang lebih panjang - yaitu dalam satu contoh kasus berubah dari 0 menjadi 5 sedangkan dalam contoh lain mereka berubah dari 0 menjadi 30 - dan tidak ada contoh yang menggunakan nilai jarang
assylias
@assylias Hmm, menarik. Saya kira saya salah paham pertanyaannya. Biarkan saya melakukan eksperimen lagi. Jadi Anda mengatakan bahwa bahkan dengan rentang yang berdekatan dari 0-30, kompiler menggunakan lookupswitch?
Vivin Paliath
@VivinPaliath: Ya, dalam pengujian saya konstanta case selalu bersebelahan, jadi saya pada dasarnya menguji sakelar pada [0, 1], [0, 1, 2], [0, 1, 2, 3] ... dll
Andrew Bissell
@VivinPaliath Tidak, bytecode selalu menggunakan tableswitch - namun kompiler JIT tampaknya tidak mengkompilasi tableswitch ke assembly dengan cara yang sama tergantung pada berapa banyak item yang dikandungnya.
assylias
6
@VivinPaliath saya bisa menjawab pertanyaan dengan lebih jelas. Saya agak keluar dari kedalaman saya ketika datang untuk mengevaluasi jawaban yang melibatkan bytecode & assembly level rendah ini. Bagi saya sepertinya perbedaan tableswitch / lookupswitch sebenarnya penting di sini, dan jawaban Anda adalah satu-satunya jawaban yang menggunakan istilah-istilah tersebut sejauh ini (meskipun yang lain mungkin mengemukakan konsep yang sama dengan terminologi yang berbeda). Plus, saya juga suka memiliki tautan JVM Spec.
Andrew Bissell
19

Karena pertanyaan sudah dijawab (kurang lebih), berikut adalah beberapa tip. Menggunakan

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

Kode itu menggunakan IC secara signifikan lebih sedikit (cache instruksi) dan akan selalu dimasukkan. Array akan berada dalam cache data L1 jika kodenya panas. Tabel pencarian hampir selalu menang. (khususnya pada microbenchmarks: D)

Sunting: jika Anda ingin metode ini menjadi hot-inlined, pertimbangkan jalur non-cepat seperti throw new ParseException()sesingkat minimum atau pindahkan mereka ke metode statis yang terpisah (karenanya buatlah sesingkat minimum). Itu adalah throw new ParseException("Unhandled power of ten " + power, 0);ide yang lemah b / c itu memakan banyak anggaran inlining untuk kode yang hanya bisa ditafsirkan - rangkaian string cukup bertele-tele dalam bytecode. Info lebih lanjut dan kasing nyata dengan ArrayList

bestsss
sumber