'Geçiş', 'if' değerinden daha mı hızlı?


242

Bir switchifade aslında bir ifadeden daha mı hızlıdır if?

Aşağıdaki kodu /Oxbayrağı ile Visual Studio 2010'un x64 C ++ derleyicisi üzerinde koştu :

#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());
}

ve şu sonuçları aldım:

Anahtar deyimi: 5261 ms
If deyimi: 5196 ms

Öğrendiklerime göre, switchifadeler dallanmayı optimize etmek için atlama tablolarını kullanıyor.

Sorular:

  1. X86 veya x64'te temel bir atlama tablosu nasıl görünür?

  2. Bu kod bir atlama tablosu kullanıyor mu?

  3. Bu örnekte neden performans farkı yok? Önemli bir performans farkının olduğu herhangi bir durum var mı?


Kodun sökülmesi:

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       

Güncelleme:

İlginç sonuçlar burada . Birinin neden daha hızlı ve birinin daha yavaş olduğundan emin değilim.


47
İnsanlar bu düşünceyi kapatmak için oy kullanıyorlar mı? İdeal koddan daha az ürettiği düşüncesinin sapkın olduğunu mükemmel bir şekilde optimize eden derleyici fikrine inanıyorlar mı? Çok fikir mu herhangi optimizasyon yerde onları rahatsız?
Crashworks

6
Bu soruda tam olarak yanlış olan ne?
Tugrul Ateş

25
Bu soruda neyin yanlış olduğunu merak eden herkese : Yeni başlayanlar için bu bir soru değil, 3 soru, yani cevapların çoğu farklı sorunları ele alıyor. Bu, her şeyi yanıtlayan herhangi bir cevabı kabul etmenin zor olacağı anlamına gelir . Ek olarak, yukarıdaki soruya tipik diz sarsıntısı tepkisi, çoğunlukla bu optimizasyon seviyesinde, neredeyse her zaman erken optimizasyon yaptığınızdan dolayı "gerçekten o kadar ilginç değil" olarak kapatmaktır . Son olarak, 5196'ya karşı 5261 aslında bakım için yeterli olmamalıdır. Mantıklı bir mantıksal kod yazın.
Lasse V. Karlsen

40
@Lasse: Bunun yerine SO'ya üç soru göndermemi gerçekten tercih eder miydiniz ? Ayrıca: -> Soruyu yanlış anladığınızdan veya yorumunuzu yanlış anladığımdan emin değilim, ancak sorumun neden bir fark olmadığını sormak için asıl amacım değil mi? (Bunun önemli bir fark olduğunu iddia ettim mi?)5196 vs. 5261 shouldn't be enough to actually care
user541686

5
@Robert: Meta yorum olduğu için üzerinde sadece 20'den fazla yorum var. Burada soru ile ilgili sadece 7 yorum var. Görüş: Burada nasıl "görüş" olduğunu anlamıyorum. Performans farkı görmememin bir nedeni var, değil mi? Sadece tat mý? Tartışma: Belki, ama benim için sağlıklı bir tartışma gibi görünüyor, SO'daki diğer yerlerde gördüğüm gibi (buna karşı bir şey olup olmadığını bana bildirin). Argümanlar: Burada tartışmalı bir şey görmüyorum ('tartışma' ile eşanlamlı olmadıkça?). Genişletilmiş tartışma: Bu meta yorumları eklerseniz.
user541686

Yanıtlar:


122

Bir derleyicinin bir anahtarda yapabileceği çeşitli optimizasyonlar vardır . Sık sık bahsedilen "atlama tablosu" çok yararlı olduğunu düşünmüyorum, çünkü sadece giriş bir şekilde sınırlanabilir zaman çalışır.

C Sözdekod bir "atlama tablosunun" için gibi bir şey olurdu bu notu pratikte derleyici masa etrafında Test girişi tablosundaki geçerli olduğundan emin olmak için eğer çeşit eklemek gerekir ki -. Ayrıca, yalnızca girişin ardışık sayıların çalıştırılması olduğu durumlarda da işe yaradığını unutmayın.

Bir anahtardaki dalların sayısı aşırı büyükse, bir derleyici, anahtarın değerleri üzerinde ikili arama kullanmak gibi şeyler yapabilir, bu da (aklımda), bazılarında performansı önemli ölçüde artırdığı için çok daha kullanışlı bir optimizasyon olacaktır. senaryolar, bir anahtar kadar geneldir ve daha büyük kod boyutu ile sonuçlanmaz. Ancak bunu görmek için, test kodunuzun herhangi bir farkı görmek için çok daha fazla şubeye ihtiyacı olacaktır.

Özel sorularınızı cevaplamak için:

  1. Clang gibi bir o görünüyor üretir bu :

    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. Bir atlama tablosu kullanmadığını söyleyebilirim - 4 karşılaştırma talimatı açıkça görülebilir:

    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) 
    

    Bir atlama tablosu tabanlı çözüm karşılaştırmayı hiç kullanmaz.

  3. Derleyicinin bir atlama tablosu oluşturmasına neden olacak kadar dal yok veya derleyiciniz bunları oluşturmuyor. Hangisi olduğundan emin değilim.

DÜZENLEME 2014 : LLVM optimize edicisini bilen kişilerden başka yerlerde atlama tablosu optimizasyonunun birçok senaryoda önemli olabileceğini söyleyen bazı tartışmalar olmuştur; söz konusu numaralandırmada birçok değere sahip bir sayımın olduğu ve değerlere karşı çok sayıda vaka olduğu durumlarda. Bununla birlikte, 2011'de yukarıda söylediğim şeyin yanındayım - çoğu zaman insanların "bir geçiş yaparsam, kaç vaka olursa olsun" aynı anda olacağını düşünürüm - ve bu tamamen yanlıştır. Bir atlama masası ile bile dolaylı atlama maliyetini alırsınız ve her vaka için tablodaki girişler için ödeme yaparsınız; ve bellek bant genişliği modern donanımda büyük bir anlaşma.

Okunabilirlik için kod yazın. Tuz değerine sahip herhangi bir derleyici bir if / else merdiveni görecek ve bunu eşdeğer bir anahtara dönüştürecek veya eğer daha hızlı olursa tersi olacaktır.


3
Soruyu gerçekten yanıtladığınız için ve yararlı bilgiler için +1. :-) Ancak, bir soru: Anladığım kadarıyla, bir atlama tablosu dolaylı atlama kullanır ; bu doğru mu? Öyleyse, daha zor ön alma / boru hattı nedeniyle genellikle daha yavaş değil mi?
user541686

1
@Mehrdad: Evet, dolaylı sıçramalar kullanıyor. Bununla birlikte, bir dolaylı atlama (beraberinde gelen boru hattı duraklamasıyla birlikte) yüzlerce doğrudan sıçramadan daha az olabilir. :)
Billy ONeal

1
@Mehrdad: Hayır, ne yazık ki. :( IF'nin her zaman daha okunabilir olduğunu düşünen insanların kampında olduğuma sevindim! :)
Billy ONeal

1
Birkaç çip - "[anahtarlar] yalnızca giriş bir şekilde sınırlandırılabiliyorsa çalışır" "girişin tabloda geçerli olduğundan emin olmak için tablonun etrafına bir tür test eklemesi gerekir. Ayrıca yalnızca belirli girişin ardışık sayılardan oluşan bir çalışma olması durumunda. ": potansiyel işaretçinin okunduğu ve yalnızca NULL olmayan bir atlama gerçekleştirilmişse seyrek olarak doldurulmuş bir tabloya sahip olmak tamamen mümkündür, aksi takdirde eğer atlanırsa varsayılan durum, sonra switchçıkar. Soren's, bu cevabı okuduktan sonra söylemek istediğim birkaç şey daha söyledi.
Tony Delroy

2
"Onun tuz değer herhangi bir derleyici eğer merdivenin bir if / else görecek ve eşdeğer bir anahtara dönüştürmek veya tam tersi" - bu iddia için herhangi bir destek? bir derleyici, ifcümlelerinizin sırasının frekans ve göreceli performans gereksinimlerini karşılamak için önceden ayarlandığını varsayabilir; burada switch, geleneksel olarak derleyicinin seçtiği ancak optimize etmek için açık bir davetiye olarak görülür. İyi nokta yeniden geçmiş atlama switch:-). Kod boyutu vakalara / aralığa bağlıdır - daha iyi olabilir. Son olarak, bazı numaralandırmalar, bit alanları ve charsenaryolar doğası gereği geçerli / sınırlıdır ve tepegözsüzdür.
Tony Delroy

47

Sorunuz için:

1. x86 veya x64'te temel bir atlama tablosu nasıl görünür?

Atlama tablosu, dizi yapısı gibi bir şeydeki etiketlere işaretçiyi tutan bellek adresidir. Aşağıdaki örnek, atlama tablolarının nasıl düzenlendiğini anlamanıza yardımcı olacaktır

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

resim açıklamasını buraya girin

Burada 00B14538 , Jump tablosunun işaretçisi ve D8 09 AB 00 gibi bir değer etiket işaretçisini temsil eder.

2.Bu kod bir atlama tablosu kullanıyor mu? Bu durumda hayır.

3.Bu örnekte neden performans farkı yok?

Performans farkı yoktur, çünkü her iki durum için talimat aynı görünüyor, atlama tablosu yok.

4.Herhangi bir performans farkının olduğu herhangi bir durum var mı?

Eğer çok uzun bir kontrol sırası varsa , bu durumda bir atlama tablosu kullanmak performansı artırır (dallanma / jmp talimatları neredeyse mükemmel tahmin etmezlerse pahalıdır ) ancak bellek maliyeti ile birlikte gelir.

Tüm karşılaştırma talimatlarının kodu da bazı boyutlara sahiptir, bu nedenle özellikle 32 bit işaretçiler veya ofsetlerle, tek bir atlama tablosu araması yürütülebilir dosyada çok daha büyük boyutlara sahip olmayabilir.

Sonuç: Derleyici böyle bir durumda yeterince akıllı ve uygun talimatları oluşturmak :)


(edit: nvm, Billy'nin cevabı zaten önerdiğim şeylere sahip. Sanırım bu güzel bir ek.) gcc -SÇıktıyı dahil etmek iyi olurdu : .long L1/ .long L2table girişleri dizisi bir hexdump'tan daha anlamlı ve birileri için daha yararlı derleyiciye nasıl bakılacağını öğrenmek istiyor. (Her ne kadar ben dolaylı bir jmp veya jcc bir demet olup olmadığını görmek için sadece anahtar koduna bakmak istiyorsunuz sanırım).
Peter Cordes

31

Derleyici, switch deyimini if-ifadesine eşdeğer bir kod olarak derlemek veya bir atlama tablosu oluşturmakta serbesttir. Muhtemelen en hızlı neyin çalıştırılacağına veya derleyici seçeneklerinde belirttiğinize bağlı olarak en küçük kodu üreteceğine göre diğerini seçecektir - en kötü durumda if-ifadeleri ile aynı hızda olacaktır

Derleyicinin en iyi seçimi yapmasına ve kodu en okunabilir hale getirmesine odaklanmasına güvenirim.

Vaka sayısı çok fazla olursa, atlama tablosu if serisinden çok daha hızlı olacaktır. Ancak, değerler arasındaki adımlar çok büyükse, atlama tablosu büyüyebilir ve derleyici bir tane oluşturmamayı seçebilir.


13
Bunun OP'nin sorusunu cevapladığını sanmıyorum. Hiç.
Billy ONeal

5
@Soren: Bu "temel soru" olsaydı, o zaman sorudaki 179 diğer satırla uğraşmazdım, sadece 1 satır olurdu. :-)
user541686

8
@Soren: OP sorusunun bir parçası olarak en az 3 numaralı alt soru görüyorum . Sadece tüm "performans" soruları için geçerli olan aynı cevabı boğdunuz - yani önce ölçmeniz gerekiyor. Belki Mehrdad'ın zaten ölçtüğünü ve bu kod parçasını sıcak nokta olarak izole ettiğini düşünün. Bu gibi durumlarda, cevabınız değersiz olmaktan daha kötüdür, gürültüdür.
Billy ONeal

2
Bir atlama tablosu nedir ve tanımınıza bağlı olmayan şey arasında bulanık bir çizgi vardır. Alt soru bölüm 3 hakkında bilgi verdim.
Soren

2
@wnoise: Tek doğru cevap buysa, herhangi bir performans sorusu sormak için asla bir neden olmazdı. Bununla birlikte, gerçek dünyada yazılımımızı ölçen bazılarımız var ve bazen bir parça kodun nasıl daha hızlı bir şekilde ölçüleceğini bilmiyoruz. Mehrdad'ın bu soruyu sormadan önce biraz çaba sarf ettiği açıktır; ve bence özel soruları yanıtlanamaz.
Billy ONeal

13

Bilgisayarınızın, anahtar test döngüsü sırasında sınama ile ilgili olmayan bazı görevleri gerçekleştirmediğini ve if sınama döngüsü sırasında daha az görev gerçekleştirmediğini nasıl anlarsınız? Test sonuçlarınız şu şekilde bir şey göstermez:

  1. fark çok küçük
  2. sadece bir sonuç var, bir seri sonuç değil
  3. çok az vaka var

Benim sonuçlarım:

Ekledim:

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

sayıcı örneğinizde hiç kullanılmadığı için döngüyü optimize etmeyecek şekilde, derleyici neden döngüyü gerçekleştirecekti? Hemen, anahtar her zaman böyle bir mikro kriterle bile kazanıyordu.

Kodunuzdaki diğer sorun:

switch (counter % 4 + 1)

geçiş döngünüzde

const size_t c = counter % 4 + 1; 

if döngünüzde. Bunu düzeltirseniz çok büyük fark. İfadeyi switch ifadesinin içine koymanın, derleyiciyi değeri yığına yerleştirmek yerine doğrudan CPU kayıtlarına göndermesine neden olduğuna inanıyorum. Bu nedenle bu, dengeli bir test değil, anahtar ifadesinin lehinedir.

Oh ve ben de testler arasındaki sayacı sıfırlamanız gerektiğini düşünüyorum. Aslında, muhtemelen orada bir şeyleri optimize edeceği için +1, +2, +3 vb yerine bir tür rastgele sayı kullanmalısınız. Rastgele sayı ile, örneğin şimdiki zamana dayalı bir sayı demek istiyorum. Aksi takdirde, derleyici her iki işlevinizi de uzun bir matematik işlemine dönüştürebilir ve herhangi bir döngüyle bile uğraşmayabilir.

Ben kod çalıştırmak önce derleyici şeyler anlayamadık emin olmak için Ryan'ın kodunu değiştirdim:

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

anahtar: 3740
ise: 3980

(birden fazla denemede benzer sonuçlar)

Ayrıca vaka / ifs sayısını 5'e düşürdüm ve anahtar fonksiyonu hala kazandı.


Idk, kanıtlayamıyorum; farklı sonuçlar alıyor musunuz?
user541686

+1: Kıyaslama zordur ve normal bir bilgisayarda tek bir seferde küçük bir zaman farkından hiçbir sonuç çıkaramazsınız. Çok sayıda test çalıştırmayı deneyebilir ve sonuçlarla ilgili bazı istatistikler yapabilirsiniz. Ya da bir emülatörde kontrollü yürütmede işlemci döngülerini saymak.
Thomas Padron-McCarthy

Er, tam olarak nerede sen ekledin printdeyimi? Tüm programın sonuna ekledim ve hiçbir fark görmedim. Ben de diğeri ile "sorun" ne olduğunu anlamıyorum ... zihin "çok büyük fark" ne olduğunu açıklayan?
user541686

1
@ BobTurbo: 45983493 12 saatten fazla. Bu bir yazım hatası mıydı?
Gus

1
harika, şimdi tekrar yapmak zorundayım :)
BobTurbo

7

MSVC gibi iyi bir optimizasyon derleyicisi aşağıdakileri üretebilir:

  1. kasalar uzun bir mesafeden ayarlanmışsa basit bir atlama masası
  2. çok fazla boşluk varsa seyrek (iki seviyeli) atlama masası
  3. vaka sayısı azsa veya değerler birbirine yakın değilse ifs dizisi
  4. vakaların yakın aralıklı birkaç grubu temsil etmesi durumunda yukarıdaki kombinasyon.

Kısacası, anahtar bir dizi ifs'den daha yavaş görünüyorsa, derleyici onu bir tanesine dönüştürebilir. Ve muhtemelen her vaka için bir karşılaştırma dizisi değil, aynı zamanda bir ikili arama ağacı olacaktır. Bir örnek için buraya bakın .


Aslında, bir derleyici, önerdiğiniz seyrek iki seviyeli çözümden daha iyi performans gösteren bir karma ve atlama ile değiştirebilir.
Alice

5

Ben cevap 2) ve bazı genel yorum yapmak. 2) Hayır, yayınladığınız montaj kodunda atlama tablosu yok. Bir atlama tablosu, atlama hedefleri tablosu ve doğrudan tablodan dizinlenmiş bir konuma atlamak için bir veya iki talimattır. Bir atlama tablosu, birçok olası anahtar varış yeri olduğunda daha anlamlı olacaktır. Belki de iyileştirici, hedef sayısı bir eşik değerinden fazla olmadıkça, eğer başka basit mantığın daha hızlı olduğunu bilir. Örneğinizi 4 yerine 20 olasılıkla tekrar deneyin.


+ # 2'nin cevabı için teşekkürler! :) (Btw, işte daha fazla olasılıkla sonuç.)
user541686

4

İlgilenmiştim ve switch ifadesini daha hızlı çalıştırmak için örneğinizle ilgili neleri değiştirebileceğime bir göz attım.

40 if ifadeleri alırsanız ve 0 durumu eklerseniz, if bloğu eşdeğer switch ifadesinden daha yavaş çalışır. Burada sonuçları var: https://www.ideone.com/KZeCz .

0 vakasının kaldırılmasının etkisi burada görülebilir: https://www.ideone.com/LFnrX .


1
Bağlantılarınız bozuldu.
TS

4

Eski (bulmak zor) bench ++ benchmark'ın bazı sonuçları:

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

Bundan görebildiğimiz şey (bu makinede, bu derleyici ile - VC ++ 9.0 x64), her iftest yaklaşık 0.7 nanosaniye sürer. Test sayısı arttıkça, zaman neredeyse doğrusal olarak mükemmel bir şekilde ölçeklenir.

Switch deyimiyle, değerler yoğun olduğu sürece 2 yönlü ve 10 yönlü testler arasında hızda neredeyse hiçbir fark yoktur. Ancak bu bile seyrek değerlerle, hala daha iyi 10 yol iki hızından daha - seyrek değerlerle 10 yönlü testi yoğun değerlerle 10 yönlü test olarak çok zaman olarak 1.6x sürer if/ else if.

Alt satır: sadece 4 yönlü bir test kullanmak size vs / ' nin performansı hakkında çok fazla şey göstermez . Bu koddan rakamlara bakacak olursak, bu bir 4 yönlü test için, iki üretmek için beklediğiniz gerçeğini interpole oldukça kolaydır oldukça (bir için ~ 2.8 nanosaniye benzer sonuçlar / ~ için 2.0 ).switchifelseifelseswitch


1
Testin kasıtlı olarak eşleşmediği veya sadece if/ elsezincirin sonunda eşleştirilemeyen bir değer arayıp bulmadığını bilmiyorsak, bunun ne yapılacağını bilmek biraz zor .. bench++10'dan sonra kaynakları bulamıyorum dakika googling.
Tony Delroy

3

Bir anahtar atlama tablosuna derlenmediğinde, anahtardan daha verimli olup olmadığını çok sık yazabilirsiniz ...

(1) vakaların tüm N için en kötü durum testinden ziyade bir siparişi varsa, üst veya alt yarıda, daha sonra her yarısında ikili arama stilinin olup olmadığını test etmek için if'nizi yazabilirsiniz ... en kötü durum N yerine logN

(2) belirli vakalar / gruplar diğer vakalardan çok daha sıksa, bu vakaları ilk önce izole etmek için if'lerinizi tasarlamak ortalama süreyi hızlandırabilir


Bu açıkça yanlıştır; derleyiciler bu optimizasyonları İKİ yapabilme yeteneğinden daha fazladır.
Alice

1
Alice, bir derleyicinin hangi vakaların beklenen iş yüklerinizdeki diğer vakalardan daha yaygın olacağını nasıl bilebilir? (C: Muhtemelen bilemez, bu yüzden böyle bir optimizasyon yapamaz.)
Brian Kennedy

(1) kolayca yapılabilir ve bazı derleyicilerde sadece ikili arama yapılarak yapılır. (2) çeşitli şekillerde tahmin edilebilir veya derleyiciye gösterilebilir. Hiç GCC'nin "muhtemel" veya "olası olmayan" ını kullanmadınız mı?
Alice

Ve bazı derleyiciler, programı istatistik toplayan ve daha sonra bu bilgilerden optimize eden bir modda çalıştırmaya izin verir.
Phil1970

2

Hayır, eğer o zaman başka atlarlarsa o zaman başka atlarlar ... Atlama masasında bir adres tablosu olur ya da bir karma veya bunun gibi bir şey kullanırdı.

Daha hızlı veya daha yavaş özneldir. Örneğin durum 1'in ilk yerine son şey olmasını sağlayabilirsiniz ve test programınız veya gerçek dünya programınız durum 1'i kullandıysa, kod çoğu zaman bu uygulama ile daha yavaş olur. Dolayısıyla, uygulamaya bağlı olarak, vaka listesini yeniden düzenlemek büyük bir fark yaratabilir.

Eğer derleyici 1-4 yerine 0-3 durumlarını kullandıysanız, derleyici bir atlama tablosu kullanmış olabilir, derleyicinin yine de + 1'inizi kaldırdığını anlaması gerekirdi. Belki de az sayıda öğeydi. Örneğin, 0 - 15 veya 0 - 31 yapmış olsaydınız, bunu bir tabloyla uygulamış veya başka bir kısayol kullanmış olabilir. Derleyici, kaynak kodun işlevselliğini karşıladığı sürece işleri nasıl uygulayacağını seçmekte serbesttir. Bu da derleyici farklılıklarına, sürüm farklılıklarına ve optimizasyon farklılıklarına dönüşüyor. Bir atlama tablosu istiyorsanız, bir atlama tablosu yapın, eğer o zaman başka bir ağaç istiyorsanız o zaman başka bir ağaç yapın. Derleyicinin karar vermesini istiyorsanız, bir switch / case deyimi kullanın.


2

Birinin neden daha hızlı ve birinin daha yavaş olduğundan emin değilim.

Aslında bunu açıklamak çok da zor değil ... Yanlış tahmin edilen dalların doğru tahmin edilen dallardan yüzlerce kat daha pahalı olduğunu hatırlarsanız.

In % 20sürümü, ilk dava / eğer hep hit olduğunu biridir. Modern CPU'lar genellikle hangi dalların alındığını ve hangilerinin alınmadığını "öğrenir", böylece bu dalın hemen hemen her döngüde nasıl davranacağını kolayca tahmin edebilirler. Bu, "if" sürümünün neden uçtuğunu açıklar; asla ilk testten sonra herhangi bir şey yürütmek zorunda değildir ve yinelemelerin çoğu için bu testin sonucunu (doğru) tahmin eder. Açıkçası "anahtar" biraz farklı uygulanır - belki de hesaplanan şube sayesinde yavaş olabilen bir atlama tablosu.

In % 21sürümü, dalları aslında tesadüfi. Bu nedenle, çoğu sadece her yinelemeyi yürütmekle kalmaz, CPU hangi yöne gideceklerini tahmin edemez. Bu, bir atlama tablosunun (veya başka bir "anahtar" optimizasyonunun) yardımcı olabileceği durumdur.

Bir kod parçasının modern bir derleyici ve CPU ile nasıl performans göstereceğini tahmin etmek çok zordur ve her nesilde daha da zorlaşır. En iyi tavsiye "denemek bile rahatsız etmeyin; her zaman profil" dir. Bu tavsiye her geçen gün daha da iyileşiyor - ve bunu görmezden gelebilecek insanlar kümesi küçülüyor.

Bunların hepsi yukarıdaki açıklamamın büyük ölçüde bir tahmin olduğunu söylemek. :-)


2
Yüzlerce kez daha yavaş nereden gelebileceğini görmüyorum. Yanlış tahmin edilen bir dalın en kötü durumu, çoğu modern CPU'da ~ 20 kat daha yavaş olacak bir boru hattı duraklamasıdır. Yüzlerce kez değil. (Tamam, eski bir NetBurst yongası kullanıyorsanız 35 kat daha yavaş olabilir ...)
Billy ONeal

@Billy: Tamam, bu yüzden biraz ileri bakıyorum. Sandy Bridge işlemcilerinde , "Her yanlış tahmin edilen dal tüm boru hattını yıkayarak yüzlerce uçuş talimatını kaybeder". Boru hatları genel olarak her nesilde derinleşiyor ...
Nemo

1
Doğru değil. P4'ün (NetBurst) 31 boru hattı aşaması vardı; Sandy Bridge önemli ölçüde daha az aşamaya sahiptir. Bence "100 kadar çalışma talimatını kaybetmek" talimat önbelleğinin geçersiz kılındığı varsayımı altındadır. Aslında gerçekleşen genel bir dolaylı atlama için, ancak bir atlama tablosu gibi bir şey için dolaylı sıçramanın hedefi talimat önbelleğinde bir yerde olabilir.
Billy ONeal

@Billy: Kabul etmediğimizi sanmıyorum. Benim ifadem şuydu: "Yanlış tahmin edilen şubeler, doğru tahmin edilen şubelerden onlarca ila yüzlerce kat daha pahalıdır". Hafif bir abartı, belki de ... Ama I-cache ve yürütme boru hattı derinliğinde isabetlerden daha fazlası var; okuduğum kadarıyla, sadece kod çözme sırası ~ 20 talimattır.
Nemo

Şube tahmin donanımı yürütme yolunu yanlış tahmin ederse, yönerge ardışık düzeninde bulunan yanlış yoldan uops, yürütmeyi durdurmadan basitçe bulundukları yerden kaldırılır. Bunun nasıl mümkünolduğu hakkında hiçbir fikrim yok(ya da yanlış yorumlayıp sorgulamadığım), ama görünüşe göreNehalem'de yanlış tahmin edilen şubeleri olan boru hattı tezgahları yok mu? (Sonra tekrar,
i7'm yok; i5'im

1

Yok. Montajcıya girip performansın gerçek ölçümlerini yaptığınız çoğu durumda sorunuz basitçe yanlıştır. Verilen örnek için düşünceniz kesin olarak çok kısa sürüyor

counter += (4 - counter % 4);

bana kullanmanız gereken doğru artış ifadesi gibi görünüyor.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.