Apakah switch
pernyataan sebenarnya lebih cepat dari if
pernyataan?
Saya menjalankan kode di bawah ini di kompilasi Visual Studio 2010 x64 C ++ dengan /Ox
bendera:
#include <stdlib.h>
#include <stdio.h>
#include <time.h>
#define MAX_COUNT (1 << 29)
size_t counter = 0;
size_t testSwitch()
{
clock_t start = clock();
size_t i;
for (i = 0; i < MAX_COUNT; i++)
{
switch (counter % 4 + 1)
{
case 1: counter += 4; break;
case 2: counter += 3; break;
case 3: counter += 2; break;
case 4: counter += 1; break;
}
}
return 1000 * (clock() - start) / CLOCKS_PER_SEC;
}
size_t testIf()
{
clock_t start = clock();
size_t i;
for (i = 0; i < MAX_COUNT; i++)
{
const size_t c = counter % 4 + 1;
if (c == 1) { counter += 4; }
else if (c == 2) { counter += 3; }
else if (c == 3) { counter += 2; }
else if (c == 4) { counter += 1; }
}
return 1000 * (clock() - start) / CLOCKS_PER_SEC;
}
int main()
{
printf("Starting...\n");
printf("Switch statement: %u ms\n", testSwitch());
printf("If statement: %u ms\n", testIf());
}
dan dapatkan hasil ini:
Beralih pernyataan: 5261 ms
Jika pernyataan: 5196 ms
Dari apa yang saya pelajari, switch
pernyataan tampaknya menggunakan tabel lompatan untuk mengoptimalkan percabangan.
Pertanyaan:
Seperti apa tampilan jump table dasar, di x86 atau x64?
Apakah kode ini menggunakan tabel lompat?
Mengapa tidak ada perbedaan kinerja dalam contoh ini? Apakah ada situasi di mana ada adalah perbedaan kinerja yang signifikan?
Pembongkaran kode:
testIf:
13FE81B10 sub rsp,48h
13FE81B14 call qword ptr [__imp_clock (13FE81128h)]
13FE81B1A mov dword ptr [start],eax
13FE81B1E mov qword ptr [i],0
13FE81B27 jmp testIf+26h (13FE81B36h)
13FE81B29 mov rax,qword ptr [i]
13FE81B2E inc rax
13FE81B31 mov qword ptr [i],rax
13FE81B36 cmp qword ptr [i],20000000h
13FE81B3F jae testIf+0C3h (13FE81BD3h)
13FE81B45 xor edx,edx
13FE81B47 mov rax,qword ptr [counter (13FE835D0h)]
13FE81B4E mov ecx,4
13FE81B53 div rax,rcx
13FE81B56 mov rax,rdx
13FE81B59 inc rax
13FE81B5C mov qword ptr [c],rax
13FE81B61 cmp qword ptr [c],1
13FE81B67 jne testIf+6Dh (13FE81B7Dh)
13FE81B69 mov rax,qword ptr [counter (13FE835D0h)]
13FE81B70 add rax,4
13FE81B74 mov qword ptr [counter (13FE835D0h)],rax
13FE81B7B jmp testIf+0BEh (13FE81BCEh)
13FE81B7D cmp qword ptr [c],2
13FE81B83 jne testIf+89h (13FE81B99h)
13FE81B85 mov rax,qword ptr [counter (13FE835D0h)]
13FE81B8C add rax,3
13FE81B90 mov qword ptr [counter (13FE835D0h)],rax
13FE81B97 jmp testIf+0BEh (13FE81BCEh)
13FE81B99 cmp qword ptr [c],3
13FE81B9F jne testIf+0A5h (13FE81BB5h)
13FE81BA1 mov rax,qword ptr [counter (13FE835D0h)]
13FE81BA8 add rax,2
13FE81BAC mov qword ptr [counter (13FE835D0h)],rax
13FE81BB3 jmp testIf+0BEh (13FE81BCEh)
13FE81BB5 cmp qword ptr [c],4
13FE81BBB jne testIf+0BEh (13FE81BCEh)
13FE81BBD mov rax,qword ptr [counter (13FE835D0h)]
13FE81BC4 inc rax
13FE81BC7 mov qword ptr [counter (13FE835D0h)],rax
13FE81BCE jmp testIf+19h (13FE81B29h)
13FE81BD3 call qword ptr [__imp_clock (13FE81128h)]
13FE81BD9 sub eax,dword ptr [start]
13FE81BDD imul eax,eax,3E8h
13FE81BE3 cdq
13FE81BE4 mov ecx,3E8h
13FE81BE9 idiv eax,ecx
13FE81BEB cdqe
13FE81BED add rsp,48h
13FE81BF1 ret
testSwitch:
13FE81C00 sub rsp,48h
13FE81C04 call qword ptr [__imp_clock (13FE81128h)]
13FE81C0A mov dword ptr [start],eax
13FE81C0E mov qword ptr [i],0
13FE81C17 jmp testSwitch+26h (13FE81C26h)
13FE81C19 mov rax,qword ptr [i]
13FE81C1E inc rax
13FE81C21 mov qword ptr [i],rax
13FE81C26 cmp qword ptr [i],20000000h
13FE81C2F jae testSwitch+0C5h (13FE81CC5h)
13FE81C35 xor edx,edx
13FE81C37 mov rax,qword ptr [counter (13FE835D0h)]
13FE81C3E mov ecx,4
13FE81C43 div rax,rcx
13FE81C46 mov rax,rdx
13FE81C49 inc rax
13FE81C4C mov qword ptr [rsp+30h],rax
13FE81C51 cmp qword ptr [rsp+30h],1
13FE81C57 je testSwitch+73h (13FE81C73h)
13FE81C59 cmp qword ptr [rsp+30h],2
13FE81C5F je testSwitch+87h (13FE81C87h)
13FE81C61 cmp qword ptr [rsp+30h],3
13FE81C67 je testSwitch+9Bh (13FE81C9Bh)
13FE81C69 cmp qword ptr [rsp+30h],4
13FE81C6F je testSwitch+0AFh (13FE81CAFh)
13FE81C71 jmp testSwitch+0C0h (13FE81CC0h)
13FE81C73 mov rax,qword ptr [counter (13FE835D0h)]
13FE81C7A add rax,4
13FE81C7E mov qword ptr [counter (13FE835D0h)],rax
13FE81C85 jmp testSwitch+0C0h (13FE81CC0h)
13FE81C87 mov rax,qword ptr [counter (13FE835D0h)]
13FE81C8E add rax,3
13FE81C92 mov qword ptr [counter (13FE835D0h)],rax
13FE81C99 jmp testSwitch+0C0h (13FE81CC0h)
13FE81C9B mov rax,qword ptr [counter (13FE835D0h)]
13FE81CA2 add rax,2
13FE81CA6 mov qword ptr [counter (13FE835D0h)],rax
13FE81CAD jmp testSwitch+0C0h (13FE81CC0h)
13FE81CAF mov rax,qword ptr [counter (13FE835D0h)]
13FE81CB6 inc rax
13FE81CB9 mov qword ptr [counter (13FE835D0h)],rax
13FE81CC0 jmp testSwitch+19h (13FE81C19h)
13FE81CC5 call qword ptr [__imp_clock (13FE81128h)]
13FE81CCB sub eax,dword ptr [start]
13FE81CCF imul eax,eax,3E8h
13FE81CD5 cdq
13FE81CD6 mov ecx,3E8h
13FE81CDB idiv eax,ecx
13FE81CDD cdqe
13FE81CDF add rsp,48h
13FE81CE3 ret
Memperbarui:
Hasil menarik di sini . Tidak yakin mengapa seseorang lebih cepat dan satu lebih lambat.
c
performance
switch-statement
assembly
jump-table
pengguna541686
sumber
sumber
5196 vs. 5261 shouldn't be enough to actually care
-> Saya tidak yakin apakah Anda salah memahami pertanyaan atau jika saya salah mengerti komentar Anda, tetapi bukankah seluruh poin dari pertanyaan saya adalah untuk bertanya mengapa tidak ada perbedaan? (Apakah saya pernah mengklaim bahwa ada perbedaan yang signifikan untuk diperhatikan?)Jawaban:
Ada beberapa optimisasi yang dapat dilakukan oleh kompiler pada sakelar. Saya tidak berpikir "lompatan-tabel" yang sering disebutkan adalah yang sangat berguna, karena hanya bekerja ketika input dapat dibatasi dengan cara tertentu.
C Pseudocode untuk "tabel lompatan" akan menjadi sesuatu seperti ini - perhatikan bahwa kompiler dalam prakteknya perlu memasukkan beberapa bentuk uji if di sekitar tabel untuk memastikan bahwa input tersebut valid dalam tabel. Perhatikan juga bahwa itu hanya bekerja dalam kasus tertentu bahwa input adalah serangkaian angka yang berurutan.
Jika jumlah cabang dalam switch sangat besar, kompiler dapat melakukan hal-hal seperti menggunakan pencarian biner pada nilai-nilai switch, yang (dalam pikiran saya) akan menjadi optimasi yang jauh lebih berguna, karena itu secara signifikan meningkatkan kinerja di beberapa skenario, adalah umum seperti saklar, dan tidak menghasilkan ukuran kode yang dihasilkan lebih besar. Tetapi untuk melihat itu, kode pengujian Anda akan membutuhkan BANYAK lebih banyak cabang untuk melihat perbedaan.
Untuk menjawab pertanyaan spesifik Anda:
Dentang menghasilkan satu yang terlihat seperti ini :
Saya dapat mengatakan bahwa itu tidak menggunakan tabel lompatan - 4 instruksi perbandingan terlihat jelas:
Solusi berbasis tabel langsung tidak menggunakan perbandingan sama sekali.
EDIT 2014 : Ada beberapa diskusi di tempat lain dari orang-orang yang akrab dengan pengoptimal LLVM yang mengatakan bahwa optimisasi tabel lompatan bisa menjadi penting dalam banyak skenario; misalnya dalam kasus di mana ada penghitungan dengan banyak nilai dan banyak kasus terhadap nilai dalam penghitungan tersebut. Yang mengatakan, saya mendukung apa yang saya katakan di atas pada tahun 2011 - terlalu sering saya melihat orang berpikir "jika saya beralih, itu akan menjadi waktu yang sama tidak peduli berapa banyak kasus yang saya miliki" - dan itu sepenuhnya salah. Bahkan dengan meja lompatan Anda mendapatkan biaya lompatan tidak langsung dan Anda membayar entri dalam tabel untuk setiap kasus; dan bandwidth memori adalah masalah besar pada perangkat keras modern.
Tulis kode untuk keterbacaan. Setiap kompiler yang bernilai garamnya akan melihat if / else jika ladder dan mengubahnya menjadi switch yang setara atau sebaliknya jika akan lebih cepat untuk melakukannya.
sumber
switch
keluar. Soren mengatakan beberapa hal lain yang ingin saya katakan setelah membaca jawaban ini.if
klausa Anda telah disesuaikan untuk menyesuaikan frekuensi dan kebutuhan kinerja relatif, di mana secaraswitch
tradisional dipandang sebagai undangan terbuka untuk mengoptimalkan namun kompiler memilih. Poin yang bagus adalah kembaliswitch
:-). Ukuran kode tergantung pada case / range - bisa lebih baik. Akhirnya, beberapa enum, bidang bit, danchar
skenario pada dasarnya sah / dibatasi & bebas biaya overhead.Untuk pertanyaan Anda:
1. Seperti apa tabel lompat dasar, di x86 atau x64?
Jump table adalah alamat memori yang menyimpan pointer ke label dalam sesuatu seperti struktur array. contoh berikut akan membantu Anda memahami bagaimana tabel lompatan ditata
Di mana 00B14538 adalah penunjuk ke tabel Langsung, dan nilai seperti D8 09 AB 00 mewakili penunjuk label.
2. Apakah kode ini menggunakan tabel lompat? Tidak dalam hal ini.
3. Mengapa tidak ada perbedaan kinerja dalam contoh ini?
Tidak ada perbedaan kinerja karena instruksi untuk kedua kasus terlihat sama, tidak ada tabel lompat.
4. Apakah ada situasi di mana terdapat perbedaan kinerja yang signifikan?
Jika Anda memiliki urutan if yang sangat panjang , dalam hal ini menggunakan tabel lompatan meningkatkan kinerja (instruksi percabangan / jmp mahal jika mereka tidak memprediksi dengan sempurna) tetapi disertai dengan biaya memori.
Kode untuk semua instruksi pembanding memiliki beberapa ukuran juga, jadi terutama dengan pointer atau offset 32-bit, pencarian tabel lompatan tunggal mungkin tidak memerlukan biaya lebih banyak ukuran dalam eksekusi.
Kesimpulan: Kompiler cukup pintar menangani kasus seperti itu dan menghasilkan instruksi yang sesuai :)
sumber
gcc -S
output: urutan entri.long L1
/.long L2
tabel lebih bermakna daripada hexdump, dan lebih bermanfaat bagi seseorang yang ingin belajar cara melihat kompiler. (Meskipun saya kira Anda hanya akan melihat kode saklar untuk melihat apakah itu jmp tidak langsung atau sekelompok jcc).Kompiler bebas untuk mengkompilasi pernyataan switch sebagai kode yang setara dengan if-statement, atau untuk membuat tabel lompatan. Ini kemungkinan akan memilih satu di lain berdasarkan apa yang akan mengeksekusi tercepat atau menghasilkan kode terkecil agak tergantung pada apa yang telah Anda tentukan dalam opsi kompiler Anda - jadi kasus terburuk itu akan menjadi kecepatan yang sama seperti jika-pernyataan
Saya akan percaya kompilator untuk melakukan pilihan terbaik dan fokus pada apa yang membuat kode paling mudah dibaca
Jika jumlah kasus menjadi sangat besar, tabel lompatan akan jauh lebih cepat daripada serangkaian if. Namun jika langkah-langkah antara nilai-nilai sangat besar, maka tabel lompatan bisa menjadi besar, dan kompiler dapat memilih untuk tidak menghasilkannya.
sumber
Bagaimana Anda tahu komputer Anda tidak melakukan beberapa tugas yang tidak terkait dengan tes selama loop tes beralih dan melakukan lebih sedikit tugas selama tes loop jika? Hasil tes Anda tidak menunjukkan apa pun sebagai:
Hasil saya:
Saya menambahkan:
sampai akhir sehingga tidak akan mengoptimalkan loop karena penghitung tidak pernah digunakan dalam contoh Anda jadi mengapa kompiler melakukan loop? Segera, saklar selalu menang bahkan dengan tolok ukur mikro.
Masalah lain dengan kode Anda adalah:
di loop switch Anda, versus
di loop if Anda. Perbedaan yang sangat besar jika Anda memperbaikinya. Saya percaya bahwa menempatkan pernyataan di dalam pernyataan switch memprovokasi compiler untuk mengirim nilai langsung ke register CPU daripada meletakkannya di tumpukan terlebih dahulu. Karena itu, ini mendukung pernyataan peralihan dan bukan tes yang seimbang.
Oh dan saya pikir Anda juga harus me-reset penghitung antar tes. Bahkan, Anda mungkin harus menggunakan semacam nomor acak bukan +1, +2, +3 dll, karena mungkin akan mengoptimalkan sesuatu di sana. Dengan angka acak, maksud saya angka berdasarkan waktu saat ini, misalnya. Jika tidak, kompiler dapat mengubah kedua fungsi Anda menjadi satu operasi matematika yang panjang dan bahkan tidak repot dengan loop apa pun.
Saya telah memodifikasi kode Ryan hanya cukup untuk memastikan kompiler tidak dapat memecahkan masalah sebelum kode dijalankan:
saklar: 3740
jika: 3980
(hasil serupa selama beberapa upaya)
Saya juga mengurangi jumlah case / ifs menjadi 5 dan fungsi switch masih menang.
sumber
print
pernyataan itu? Saya menambahkannya di akhir seluruh program dan tidak melihat perbedaan. Saya juga tidak mengerti apa "masalah" dengan yang lain adalah ... pikiran menjelaskan apa "perbedaan sangat besar" itu?Kompiler pengoptimal yang bagus seperti MSVC dapat menghasilkan:
Singkatnya, jika saklar terlihat lebih lambat dari serangkaian ifs, kompiler mungkin hanya mengubahnya menjadi satu. Dan itu mungkin bukan hanya urutan perbandingan untuk setiap kasus, tetapi pohon pencarian biner. Lihat di sini untuk contoh.
sumber
Saya akan menjawab 2) dan membuat beberapa komentar umum. 2) Tidak, tidak ada tabel lompat di kode perakitan yang telah Anda posting. Tabel lompat adalah tabel tujuan lompat, dan satu atau dua instruksi untuk melompat langsung ke lokasi yang diindeks dari tabel. Tabel lompatan akan lebih masuk akal bila ada banyak kemungkinan tujuan peralihan. Mungkin pengoptimal tahu bahwa logika sederhana jika lagi lebih cepat kecuali jumlah tujuan lebih besar dari beberapa ambang batas. Coba contoh Anda lagi dengan mengatakan 20 kemungkinan alih-alih 4.
sumber
Saya tertarik, dan melihat apa yang bisa saya ubah tentang contoh Anda untuk membuatnya menjalankan pernyataan switch lebih cepat.
Jika Anda mendapatkan pernyataan 40 if, dan menambahkan case 0, maka blok if akan berjalan lebih lambat dari statement switch yang setara. Saya mendapatkan hasilnya di sini: https://www.ideone.com/KZeCz .
Efek dari menghapus 0 case dapat dilihat di sini: https://www.ideone.com/LFnrX .
sumber
Berikut adalah beberapa hasil dari benchmark benchmark ++ yang lama (sekarang sulit ditemukan):
Apa yang dapat kita lihat dari ini adalah bahwa (pada mesin ini, dengan kompiler ini - VC ++ 9.0 x64), setiap
if
pengujian membutuhkan waktu sekitar 0,7 nanodetik. Seiring dengan meningkatnya jumlah tes, skala waktu hampir sempurna secara linear.Dengan pernyataan switch, hampir tidak ada perbedaan dalam kecepatan antara tes 2 arah dan 10 arah, selama nilainya padat. Tes 10 arah dengan nilai jarang membutuhkan waktu sekitar 1.6x lebih banyak daripada tes 10 arah dengan nilai padat - tetapi bahkan dengan nilai jarang, masih lebih baik dari dua kali kecepatan 10 arah
if
/else if
.Intinya: menggunakan hanya tes 4 arah tidak akan benar-benar menunjukkan banyak tentang kinerja
switch
vsif
/else
. Jika Anda melihat angka-angka dari kode ini, cukup mudah untuk menginterpolasi fakta bahwa untuk tes 4 arah, kami berharap keduanya menghasilkan hasil yang sangat mirip (~ 2,8 nanodetik untukif
/else
, ~ 2,0 untukswitch
).sumber
if
/else
rantai vs hamburan mereka dll. Tidak dapat menemukanbench++
sumber setelah 10 menit googling.Perhatikan bahwa ketika sebuah switch TIDAK dikompilasi ke tabel lompatan, Anda dapat sangat sering menulis jika lebih efisien daripada switch ...
(1) jika kasing memiliki urutan, daripada pengujian kasing terburuk untuk semua N, Anda dapat menulis jika untuk menguji apakah di bagian atas atau bawah, maka di setiap setengahnya, gaya pencarian biner ... menghasilkan kasus terburuk adalah logN daripada N
(2) jika kasus / kelompok tertentu jauh lebih sering daripada kasus lain, maka merancang jika Anda untuk mengisolasi kasus-kasus tersebut terlebih dahulu dapat mempercepat waktu rata-rata melalui
sumber
Tidak ada ini jika kemudian lompat yang lain jika kemudian lompat yang lain ... Tabel lompat akan memiliki daftar alamat atau menggunakan hash atau sesuatu seperti itu.
Lebih cepat atau lebih lambat adalah subyektif. Misalnya, Anda dapat membuat case 1 menjadi hal terakhir alih-alih yang pertama dan jika program pengujian atau program dunia nyata Anda menggunakan case 1 sebagian besar waktu kode akan lebih lambat dengan implementasi ini. Jadi hanya mengatur ulang daftar kasus, tergantung pada implementasinya, dapat membuat perbedaan besar.
Jika Anda menggunakan case 0-3 dan bukan 1-4, kompiler mungkin menggunakan tabel lompatan, kompiler seharusnya menemukan cara menghapus +1 Anda. Mungkin itu adalah sejumlah kecil item. Seandainya Anda membuatnya 0 - 15 atau 0 - 31 misalnya, mungkin telah menerapkannya dengan sebuah tabel atau menggunakan beberapa cara pintas lainnya. Kompiler bebas memilih bagaimana mengimplementasikannya selama memenuhi fungsionalitas kode sumber. Dan ini masuk ke perbedaan kompiler dan perbedaan versi dan perbedaan optimasi. Jika Anda ingin tabel lompat, buat tabel lompat, jika Anda ingin pohon if-then-else membuat pohon if-then-else. Jika Anda ingin kompilator memutuskan, gunakan pernyataan sakelar / kasus.
sumber
Itu sebenarnya tidak terlalu sulit untuk dijelaskan ... Jika Anda ingat bahwa cabang yang salah prediksi adalah puluhan hingga ratusan kali lebih mahal daripada cabang yang diprediksi dengan benar.
Dalam
% 20
versi ini, yang pertama / jika selalu yang yang hits. CPU modern "mempelajari" cabang mana yang biasanya diambil dan mana yang tidak, sehingga mereka dapat dengan mudah memprediksi bagaimana cabang ini akan berperilaku pada hampir setiap iterasi dari loop. Itu menjelaskan mengapa versi "jika" terbang; itu tidak pernah harus melakukan apa pun melewati tes pertama, dan itu (dengan benar) memprediksi hasil tes itu untuk sebagian besar iterasi. Jelas "saklar" diimplementasikan sedikit berbeda - mungkin bahkan tabel lompatan, yang bisa lambat berkat cabang yang dikomputasi.Dalam
% 21
versi, cabang-cabang pada dasarnya acak. Jadi tidak hanya banyak dari mereka yang menjalankan setiap iterasi, CPU tidak bisa menebak ke mana mereka akan pergi. Ini adalah kasus di mana tabel lompatan (atau optimasi "switch" lainnya) cenderung membantu.Sangat sulit untuk memprediksi bagaimana sepotong kode akan tampil dengan kompiler dan CPU modern, dan semakin sulit dengan setiap generasi. Saran terbaik adalah "jangan repot-repot mencoba; selalu profil". Nasihat itu menjadi lebih baik - dan sekelompok orang yang dapat mengabaikannya dengan sukses menjadi semakin kecil - setiap tahun.
Semuanya mengatakan bahwa penjelasan saya di atas sebagian besar merupakan dugaan. :-)
sumber
Tidak ada Dalam kebanyakan kasus tertentu di mana Anda pergi ke assembler dan melakukan pengukuran kinerja nyata pertanyaan Anda hanyalah salah. Sebagai contoh, pemikiran Anda terlalu pendek
Bagi saya, ini adalah ekspresi kenaikan yang benar yang harus Anda gunakan.
sumber