Apakah 'beralih' lebih cepat dari 'jika'?

242

Apakah switchpernyataan sebenarnya lebih cepat dari ifpernyataan?

Saya menjalankan kode di bawah ini di kompilasi Visual Studio 2010 x64 C ++ dengan /Oxbendera:

#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, switchpernyataan tampaknya menggunakan tabel lompatan untuk mengoptimalkan percabangan.

Pertanyaan:

  1. Seperti apa tampilan jump table dasar, di x86 atau x64?

  2. Apakah kode ini menggunakan tabel lompat?

  3. 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.

pengguna541686
sumber
47
Apa yang orang-orang pilih untuk menutup pemikiran ini? Apakah mereka begitu percaya pada gagasan kompiler yang mengoptimalkan secara sempurna sehingga setiap pemikiran tentang menghasilkan kode yang kurang ideal adalah bid'ah? Apakah gagasan tentang setiap optimasi di mana saja menyinggung perasaan mereka?
Crashworks
6
Apa sebenarnya yang salah dengan pertanyaan ini?
Tugrul Ates
25
Untuk siapa pun bertanya-tanya apa yang salah dengan pertanyaan ini : Sebagai permulaan, itu bukan sebuah pertanyaan, itu adalah 3 pertanyaan, yang berarti bahwa banyak jawaban sekarang menangani masalah-masalah yang berbeda. Ini berarti bahwa akan sulit untuk menerima jawaban apa pun yang menjawab segalanya . Selain itu, reaksi spontan terhadap pertanyaan di atas adalah menutupnya sebagai "tidak terlalu menarik" karena sebagian besar pada kenyataan bahwa pada tingkat optimasi ini, Anda hampir selalu mengoptimalkan secara prematur . Terakhir, 5196 vs 5261 seharusnya tidak cukup untuk benar-benar peduli. Tulis kode logis yang masuk akal.
Lasse V. Karlsen
40
@Lasse: Apakah Anda benar - benar lebih suka saya mengirim tiga pertanyaan pada SO sebagai gantinya? Juga: 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?)
user541686
5
@ Robert: Ya, itu hanya memiliki lebih dari 20 komentar karena itu adalah meta-komentar. Hanya ada 7 komentar yang terkait dengan pertanyaan di sini. Opini: Saya tidak melihat bagaimana ada "pendapat" di sini. Ada alasan mengapa saya tidak melihat perbedaan kinerja, bukan? Apakah hanya rasa? Debat: Mungkin, tapi sepertinya debat yang sehat bagi saya, seperti yang pernah saya lihat di tempat lain di SO (beri tahu saya kalau ada yang bertentangan dengan itu). Argumen: Saya tidak melihat sesuatu yang argumentatif di sini (kecuali Anda menganggapnya sebagai sinonim untuk 'debat'?). Diskusi diperpanjang: Jika Anda memasukkan meta-komentar ini.
user541686

Jawaban:

122

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:

  1. Dentang menghasilkan satu yang terlihat seperti ini :

    test_switch(char):                       # @test_switch(char)
            movl    %edi, %eax
            cmpl    $19, %edi
            jbe     .LBB0_1
            retq
    .LBB0_1:
            jmpq    *.LJTI0_0(,%rax,8)
            jmp     void call<0u>()         # TAILCALL
            jmp     void call<1u>()         # TAILCALL
            jmp     void call<2u>()         # TAILCALL
            jmp     void call<3u>()         # TAILCALL
            jmp     void call<4u>()         # TAILCALL
            jmp     void call<5u>()         # TAILCALL
            jmp     void call<6u>()         # TAILCALL
            jmp     void call<7u>()         # TAILCALL
            jmp     void call<8u>()         # TAILCALL
            jmp     void call<9u>()         # TAILCALL
            jmp     void call<10u>()        # TAILCALL
            jmp     void call<11u>()        # TAILCALL
            jmp     void call<12u>()        # TAILCALL
            jmp     void call<13u>()        # TAILCALL
            jmp     void call<14u>()        # TAILCALL
            jmp     void call<15u>()        # TAILCALL
            jmp     void call<16u>()        # TAILCALL
            jmp     void call<17u>()        # TAILCALL
            jmp     void call<18u>()        # TAILCALL
            jmp     void call<19u>()        # TAILCALL
    .LJTI0_0:
            .quad   .LBB0_2
            .quad   .LBB0_3
            .quad   .LBB0_4
            .quad   .LBB0_5
            .quad   .LBB0_6
            .quad   .LBB0_7
            .quad   .LBB0_8
            .quad   .LBB0_9
            .quad   .LBB0_10
            .quad   .LBB0_11
            .quad   .LBB0_12
            .quad   .LBB0_13
            .quad   .LBB0_14
            .quad   .LBB0_15
            .quad   .LBB0_16
            .quad   .LBB0_17
            .quad   .LBB0_18
            .quad   .LBB0_19
            .quad   .LBB0_20
            .quad   .LBB0_21
    
  2. Saya dapat mengatakan bahwa itu tidak menggunakan tabel lompatan - 4 instruksi perbandingan terlihat jelas:

    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) 
    

    Solusi berbasis tabel langsung tidak menggunakan perbandingan sama sekali.

  3. Entah tidak cukup cabang untuk membuat kompiler menghasilkan tabel lompatan, atau kompiler Anda tidak menghasilkannya. Saya tidak yakin yang mana.

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.

Billy ONeal
sumber
3
+1 untuk benar-benar menjawab pertanyaan, dan untuk info bermanfaat. :-) Namun, sebuah pertanyaan: Dari apa yang saya mengerti, tabel lompatan menggunakan lompatan tidak langsung ; Apakah itu benar? Jika demikian, bukankah itu biasanya lebih lambat karena prefetching / pipelining yang lebih sulit?
user541686
1
@Mehrdad: Ya, ia menggunakan lompatan tidak langsung. Namun, satu lompatan tidak langsung (dengan kios salurannya) mungkin kurang dari ratusan lompatan langsung. :)
Billy ONeal
1
@Mehrdad: Tidak, sayangnya. :( Saya senang saya berada di perkemahan orang-orang yang selalu berpikir JIKA lebih mudah dibaca! :)
Billy ONeal
1
Beberapa gurauan - "[switch] hanya berfungsi ketika input dapat dibatasi" "dengan cara menyisipkan beberapa bentuk uji if di sekitar tabel untuk memastikan bahwa input tersebut valid dalam tabel. Perhatikan juga bahwa itu hanya bekerja di spesifik dalam hal input adalah serangkaian angka berurutan. ": sangat mungkin untuk memiliki tabel dengan populasi yang jarang, di mana penunjuk potensial dibaca dan hanya jika non-NULL adalah lompatan yang dilakukan, jika tidak, kasing standar jika ada yang dilompati, lalu switchkeluar. Soren mengatakan beberapa hal lain yang ingin saya katakan setelah membaca jawaban ini.
Tony Delroy
2
"Setiap kompiler yang bernilai garamnya akan melihat if / else jika ladder dan mengubahnya menjadi switch yang setara atau sebaliknya" - ada dukungan untuk pernyataan ini? kompiler mungkin berasumsi bahwa urutan ifklausa Anda telah disesuaikan untuk menyesuaikan frekuensi dan kebutuhan kinerja relatif, di mana secara switchtradisional dipandang sebagai undangan terbuka untuk mengoptimalkan namun kompiler memilih. Poin yang bagus adalah kembali switch:-). Ukuran kode tergantung pada case / range - bisa lebih baik. Akhirnya, beberapa enum, bidang bit, dan charskenario pada dasarnya sah / dibatasi & bebas biaya overhead.
Tony Delroy
47

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

00B14538  D8 09 AB 00 D8 09 AB 00 D8 09 AB 00 D8 09 AB 00  Ø.«.Ø.«.Ø.«.Ø.«.
00B14548  D8 09 AB 00 D8 09 AB 00 D8 09 AB 00 00 00 00 00  Ø.«.Ø.«.Ø.«.....
00B14558  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
00B14568  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................

masukkan deskripsi gambar di sini

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 :)

crypted
sumber
(edit: nvm, jawaban Billy sudah memiliki apa yang saya sarankan. Saya kira ini adalah suplemen yang bagus.) Akan lebih baik untuk memasukkan gcc -Soutput: urutan entri .long L1/ .long L2tabel 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).
Peter Cordes
31

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.

Soren
sumber
13
Saya tidak berpikir ini menjawab pertanyaan OP. Sama sekali.
Billy ONeal
5
@ Koren: Jika itu adalah "pertanyaan dasar" maka saya tidak akan terganggu dengan 179 baris lain dalam pertanyaan, itu hanya 1 baris. :-)
user541686
8
@ Koren: Saya melihat setidaknya 3 sub-pertanyaan bernomor sebagai bagian dari pertanyaan OP. Anda hanya melenguh jawaban yang sama persis yang berlaku untuk semua pertanyaan "kinerja" - yaitu, yang harus Anda ukur terlebih dahulu. Pertimbangkan bahwa mungkin Mehrdad sudah mengukur, dan telah mengisolasi bagian kode ini menjadi hot spot. Dalam kasus seperti itu, jawaban Anda lebih buruk daripada tidak berharga, itu adalah kebisingan.
Billy ONeal
2
Ada garis yang kabur antara apa itu tabel lompat dan apa yang tidak tergantung pada definisi Anda. Saya telah memberikan informasi pada sub-pertanyaan bagian 3.
Soren
2
@wnoise: Jika itu satu-satunya jawaban yang tepat maka tidak akan pernah ada alasan untuk pernah mengajukan pertanyaan kinerja. Namun, ada beberapa dari kita di dunia nyata yang mengukur perangkat lunak kita, dan kita kadang-kadang tidak tahu bagaimana membuat sepotong kode lebih cepat begitu diukur. Sudah jelas bahwa Mehrdad berupaya untuk pertanyaan ini sebelum mengajukannya; dan saya pikir pertanyaan spesifiknya lebih dari dapat dijawab.
Billy ONeal
13

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:

  1. perbedaannya sangat kecil
  2. hanya ada satu hasil, bukan serangkaian hasil
  3. ada terlalu sedikit kasus

Hasil saya:

Saya menambahkan:

printf("counter: %u\n", counter);

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:

switch (counter % 4 + 1)

di loop switch Anda, versus

const size_t c = counter % 4 + 1; 

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:

#include <stdlib.h>
#include <stdio.h>
#include <time.h>

#define MAX_COUNT (1 << 26)
size_t counter = 0;

long long testSwitch()
{
    clock_t start = clock();
    size_t i;
    for (i = 0; i < MAX_COUNT; i++)
    {
        const size_t c = rand() % 20 + 1;

        switch (c)
        {
                case 1: counter += 20; break;
                case 2: counter += 33; break;
                case 3: counter += 62; break;
                case 4: counter += 15; break;
                case 5: counter += 416; break;
                case 6: counter += 3545; break;
                case 7: counter += 23; break;
                case 8: counter += 81; break;
                case 9: counter += 256; break;
                case 10: counter += 15865; break;
                case 11: counter += 3234; break;
                case 12: counter += 22345; break;
                case 13: counter += 1242; break;
                case 14: counter += 12341; break;
                case 15: counter += 41; break;
                case 16: counter += 34321; break;
                case 17: counter += 232; break;
                case 18: counter += 144231; break;
                case 19: counter += 32; break;
                case 20: counter += 1231; break;
        }
    }
    return 1000 * (long long)(clock() - start) / CLOCKS_PER_SEC;
}

long long testIf()
{
    clock_t start = clock();
    size_t i;
    for (i = 0; i < MAX_COUNT; i++)
    {
        const size_t c = rand() % 20 + 1;
        if (c == 1) { counter += 20; }
        else if (c == 2) { counter += 33; }
        else if (c == 3) { counter += 62; }
        else if (c == 4) { counter += 15; }
        else if (c == 5) { counter += 416; }
        else if (c == 6) { counter += 3545; }
        else if (c == 7) { counter += 23; }
        else if (c == 8) { counter += 81; }
        else if (c == 9) { counter += 256; }
        else if (c == 10) { counter += 15865; }
        else if (c == 11) { counter += 3234; }
        else if (c == 12) { counter += 22345; }
        else if (c == 13) { counter += 1242; }
        else if (c == 14) { counter += 12341; }
        else if (c == 15) { counter += 41; }
        else if (c == 16) { counter += 34321; }
        else if (c == 17) { counter += 232; }
        else if (c == 18) { counter += 144231; }
        else if (c == 19) { counter += 32; }
        else if (c == 20) { counter += 1231; }
    }
    return 1000 * (long long)(clock() - start) / CLOCKS_PER_SEC;
}

int main()
{
    srand(time(NULL));
    printf("Starting...\n");
    printf("Switch statement: %lld ms\n", testSwitch()); fflush(stdout);
    printf("counter: %d\n", counter);
    counter = 0;
    srand(time(NULL));
    printf("If     statement: %lld ms\n", testIf()); fflush(stdout);
    printf("counter: %d\n", counter);
} 

saklar: 3740
jika: 3980

(hasil serupa selama beberapa upaya)

Saya juga mengurangi jumlah case / ifs menjadi 5 dan fungsi switch masih menang.

BobTurbo
sumber
Idk, saya tidak bisa membuktikannya; apakah Anda mendapatkan hasil yang berbeda?
user541686
+1: Benchmarking itu sulit, dan Anda benar-benar tidak bisa menarik kesimpulan dari perbedaan waktu yang kecil pada sekali jalan di komputer biasa. Anda dapat mencoba menjalankan sejumlah besar tes dan melakukan beberapa statistik pada hasilnya. Atau menghitung siklus prosesor pada eksekusi terkontrol dalam emulator.
Thomas Padron-McCarthy
Eh, tepatnya di mana Anda menambahkan printpernyataan 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?
user541686
1
@BobTurbo: 45983493 lebih dari 12 jam. Apakah itu salah cetak?
Gus
1
Hebat, sekarang saya harus melakukannya lagi :)
BobTurbo
7

Kompiler pengoptimal yang bagus seperti MSVC dapat menghasilkan:

  1. tabel lompatan sederhana jika kasing diatur dalam jarak jauh yang bagus
  2. tabel lompat jarang (dua tingkat) jika ada banyak celah
  3. serangkaian ifs jika jumlah kasus kecil atau nilainya tidak berdekatan
  4. kombinasi di atas jika kasus-kasus tersebut mewakili beberapa kelompok dengan jarak yang berdekatan.

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.

Igor Skochinsky
sumber
Sebenarnya, sebuah kompiler juga dapat menggantinya dengan hash dan lompatan, yang berkinerja lebih baik daripada solusi dua tingkat yang Anda usulkan.
Alice
5

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.

Bill Forster
sumber
+1 terima kasih atas jawaban ke # 2! :) (Btw, berikut adalah hasil dengan lebih banyak kemungkinan.)
user541686
4

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 .

Ryan Gross
sumber
1
Tautan Anda rusak.
TS
4

Berikut adalah beberapa hasil dari benchmark benchmark ++ yang lama (sekarang sulit ditemukan):

Test Name:   F000003                         Class Name:  Style
CPU Time:       0.781  nanoseconds           plus or minus     0.0715
Wall/CPU:        1.00  ratio.                Iteration Count:  1677721600
Test Description:
 Time to test a global using a 2-way if/else if statement
 compare this test with F000004

Test Name:   F000004                         Class Name:  Style
CPU Time:        1.53  nanoseconds           plus or minus     0.0767
Wall/CPU:        1.00  ratio.                Iteration Count:  1677721600
Test Description:
 Time to test a global using a 2-way switch statement
 compare this test with F000003

Test Name:   F000005                         Class Name:  Style
CPU Time:        7.70  nanoseconds           plus or minus      0.385
Wall/CPU:        1.00  ratio.                Iteration Count:  1677721600
Test Description:
 Time to test a global using a 10-way if/else if statement
 compare this test with F000006

Test Name:   F000006                         Class Name:  Style
CPU Time:        2.00  nanoseconds           plus or minus     0.0999
Wall/CPU:        1.00  ratio.                Iteration Count:  1677721600
Test Description:
 Time to test a global using a 10-way switch statement
 compare this test with F000005

Test Name:   F000007                         Class Name:  Style
CPU Time:        3.41  nanoseconds           plus or minus      0.171
Wall/CPU:        1.00  ratio.                Iteration Count:  1677721600
Test Description:
 Time to test a global using a 10-way sparse switch statement
 compare this test with F000005 and F000006

Apa yang dapat kita lihat dari ini adalah bahwa (pada mesin ini, dengan kompiler ini - VC ++ 9.0 x64), setiap ifpengujian 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 switchvs if/ 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 untuk if/ else, ~ 2,0 untuk switch).

Jerry Coffin
sumber
1
Agak sulit untuk mengetahui apa yang harus dilakukan jika kita tidak tahu apakah tes sengaja mencari nilai yang tidak cocok atau hanya cocok pada akhir if/ elserantai vs hamburan mereka dll. Tidak dapat menemukan bench++sumber setelah 10 menit googling.
Tony Delroy
3

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

Brian Kennedy
sumber
Ini sangat tidak benar; kompiler lebih dari mampu membuat KEDUA optimasi ini.
Alice
1
Alice, bagaimana kompiler seharusnya tahu kasus mana yang akan terjadi lebih umum daripada kasus lain dalam beban kerja yang Anda harapkan? (A: Tidak mungkin tahu, jadi tidak mungkin melakukan optimasi seperti itu.)
Brian Kennedy
(1) dapat dilakukan dengan mudah, dan dilakukan dalam beberapa kompiler, hanya dengan melakukan pencarian biner. (2) dapat diprediksi dalam berbagai cara, atau ditunjukkan ke kompiler. Pernahkah Anda menggunakan "kemungkinan" atau "tidak mungkin" GCC?
Alice
Dan beberapa kompiler memungkinkan untuk menjalankan program dalam mode yang mengumpulkan statistik dan kemudian mengoptimalkan dari informasi itu.
Phil1970
2

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.

old_timer
sumber
2

Tidak yakin mengapa seseorang lebih cepat dan satu lebih lambat.

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 % 20versi 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 % 21versi, 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. :-)

Nemo
sumber
2
Saya tidak melihat dari mana ratusan kali lebih lambat dapat berasal. Kasus terburuk dari cabang yang salah prediksi adalah kios saluran pipa, yang akan ~ 20 kali lebih lambat pada kebanyakan CPU modern. Tidak ratusan kali. (Oke, jika Anda menggunakan chip NetBurst lama mungkin 35x lebih lambat ...)
Billy ONeal
@Illy: Oke, jadi saya melihat ke depan sedikit. Pada prosesor Sandy Bridge , "Setiap cabang yang salah duga akan menyiram seluruh pipa, kehilangan pekerjaan hingga seratus atau lebih instruksi dalam penerbangan". Jalur pipa benar-benar semakin dalam dengan setiap generasi, secara umum ...
Nemo
1
Tidak benar. P4 (NetBurst) memiliki 31 tahapan pipa; Sandy Bridge memiliki tahapan yang jauh lebih sedikit. Saya pikir "kehilangan kerja 100 atau lebih instruksi" berada di bawah asumsi bahwa cache instruksi akan dibatalkan. Untuk lompatan tidak langsung umum yang sebenarnya terjadi, tetapi untuk sesuatu seperti tabel lompatan kemungkinan target lompatan tidak langsung terletak di suatu tempat di cache instruksi.
Billy ONeal
@Illy: Saya tidak berpikir kita tidak setuju. Pernyataan saya adalah: "Cabang yang salah duga puluhan hingga ratusan kali lebih mahal daripada cabang yang diprediksi dengan benar". Sedikit berlebihan, mungkin ... Tapi ada lebih banyak hal yang terjadi daripada sekadar hit di I-cache dan kedalaman pipa eksekusi; dari apa yang saya baca, antrian untuk decode saja ~ 20 instruksi.
Nemo
Jika perangkat keras prediksi cabang salah memperkirakan jalur eksekusi, uops dari jalur yang salah yang ada di dalam pipa instruksi hanya dihapus di mana mereka berada, tanpa menghentikan eksekusi. Saya tidak tahu bagaimana ini mungkin (atau apakah saya salah menafsirkannya), tetapi ternyata tidak ada kios pipa dengan cabang yang salah duga di Nehalem? (Kemudian lagi, saya tidak punya i7; Saya punya i5, jadi ini tidak berlaku untuk kasus saya.)
user541686
1

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

counter += (4 - counter % 4);

Bagi saya, ini adalah ekspresi kenaikan yang benar yang harus Anda gunakan.

Jens Gustedt
sumber