Bir anahtar neden c / c ++ 'da zincirleme ile aynı şekilde optimize edilmez?


39

Aşağıdaki kare uygulaması zincirleme if deyimi beklediğiniz gibi bir dizi cmp / je ifadeleri üretir:

int square(int num) {
    if (num == 0){
        return 0;
    } else if (num == 1){
        return 1;
    } else if (num == 2){
        return 4;
    } else if (num == 3){
        return 9;
    } else if (num == 4){
        return 16;
    } else if (num == 5){
        return 25;
    } else if (num == 6){
        return 36;
    } else if (num == 7){
        return 49;
    } else {
        return num * num;
    }
}

Aşağıdakiler geri dönüş için bir veri tablosu oluşturur:

int square_2(int num) {
    switch (num){
        case 0: return 0;
        case 1: return 1;
        case 2: return 4;
        case 3: return 9;
        case 4: return 16;
        case 5: return 25;
        case 6: return 36;
        case 7: return 49;
        default: return num * num;
    }
}

Gcc neden ilkini en altta optimize edemiyor?

Referans için sökme: https://godbolt.org/z/UP_igi

EDIT: ilginç bir şekilde, MSVC anahtar durumu için bir veri tablosu yerine bir atlama tablosu oluşturur. Ve şaşırtıcı bir şekilde, clang onları aynı sonuca göre optimize eder.


3
Ne demek "tanımsız davranış"? Gözlenebilir davranış aynı olduğu sürece, derleyici istediği montaj / makine kodunu üretebilir
bolov

2
@ user207421 returns yoksayılıyor ; vakaların hiçbiri yoktur breaks, dolayısıyla anahtarın belirli bir yürütme sırası da vardır. İf / else zincirinin her dalda getirisi varsa, bu durumda anlambilim eşdeğerdir. Optimizasyon imkansız değil . Karşı örnek olarak icc hiçbir işlevi optimize etmez.
user1810087

9
Belki de en basit cevap ... gcc bu yapıyı görüp optimize edememiştir (henüz).
user1810087

3
@ User1810087 ile katılıyorum. Derleyici iyileştirme işleminin geçerli sınırını buldunuz. Şu anda optimize edilebilir olarak tanınmayan bir alt-alt durum (bazı derleyiciler tarafından). Aslında, diğer tüm zincirler bu şekilde optimize edilemez, sadece SAME değişkeninin sabit değerlere karşı test edildiği altküme.
Roberto Caboni

1
İf-else yukarıdan aşağıya farklı bir yürütme sırasına sahiptir. Yine de, ifadeler makine kodunu iyileştirmediyse kodu değiştirmek. Diğer yandan, anahtarın önceden tanımlanmış bir yürütme sırası yoktur ve esasen sadece yüceltilmiş bir goto atlama masasıdır. Bununla birlikte, bir derleyicinin gözlemlenebilir davranış hakkında akıl yürütmesine izin verilir, bu nedenle if-else sürümünün zayıf optimizasyonu oldukça hayal kırıklığı yaratır.
Lundin

Yanıtlar:


29

İçin oluşturulan kod switch-case geleneksel olarak bir atlama tablosu kullanır. Bu durumda, bir arama tablosundan doğrudan geri dönüş, buradaki her durumun bir dönüş içerdiği gerçeğini kullanan bir optimizasyon gibi görünmektedir. Standart bu etki için hiçbir garanti vermezse de, bir derleyicinin geleneksel bir anahtar kutusu için bir atlama tablosu yerine bir dizi karşılaştırma üretmesi şaşırtıcı olurdu.

Şimdi geliyor if-else, tam tersi. Birlikte switch-casesabit zamanda çalıştırır, bağımsız dal sayısı, if-elsedal daha az sayıda için optimize edilmiştir. Burada, derleyicinin yazdığınız sırayla bir dizi karşılaştırma yapmasını beklersiniz.

if-elseÇoğu çağrının diğer değerler square()için 0veya 1nadiren yapılmasını beklediğim için kullanmış olsaydım , bunu bir tablo aramasına 'optimize etmek' aslında kodumun beklediğimden daha yavaş çalışmasına neden olabilir, ifbunun yerine bir amacımı yenmek a switch. Tartışmalı olmasına rağmen, GCC'nin doğru şeyi yaptığını hissediyorum ve clang optimizasyonunda aşırı agresif davranıyor.

Birisi, yorumlarda, clang'ın bu optimizasyonu yaptığı ve arama tablosu tabanlı kod da oluşturduğu bir bağlantı paylaştı if-else. Vaka sayısını clang ile sadece ikiye (ve varsayılana) indirdiğimizde dikkate değer bir şey olur. Bir kez daha hem if ve switch için aynı kodu üretir, ancak bu kez, her ikisi için de arama tablosu yaklaşımı yerine karşılaştırma ve taşıma işlemlerine geçer . Bu, anahtar tercih eden clang'ın bile vaka sayısı az olduğunda 'if' deseninin daha uygun olduğunu biliyor demektir!

Özetle, bir karşılaştırma dizisi if-elseve bir atlama tablosu switch-case, derleyicilerin takip etme eğiliminde olduğu ve geliştiricilerin kod yazarken bekledikleri standart modeldir. Bununla birlikte, bazı özel durumlar için, bazı derleyiciler bu kalıbı daha iyi optimizasyon sağladığını düşündükleri şekilde kırmayı seçebilir. Diğer derleyiciler, görünüşe göre en düşük düzeyde olsa bile, geliştiriciye ne istediğini bilmesine güvenerek, yine de desene yapışmayı seçebilirler. Her ikisi de kendi avantajları ve dezavantajları olan geçerli yaklaşımlardır.


2
Evet, optimizasyon çok kenarlı bir kılıçtır: Ne yazdıklarını, ne istediklerini, ne aldıklarını ve bunun için kime küfrettiğimizi.
Deduplicator

1
“... sonra bunu bir tablo aramasına 'optimize etmek' aslında kodumun beklediğimden daha yavaş çalışmasına neden olacak ..." Bunun için bir gerekçe sağlayabilir misiniz? Neden bir atlama tablosu iki olası koşullu daldan daha yavaş olabilir (girişleri 0ve ile kontrol etmek için 1)?
Cody Gray

@CodyGray Sayma döngüleri seviyesine sahip olmadığımı itiraf etmeliyim - sadece bir işaretçi aracılığıyla bellekten gelen yükün bir karşılaştırma ve atlamadan daha fazla döngü alabileceği hissine kapıldım, ama yanlış olabilirim. Ancak, umarım bu durumda bile, en azından '0' için bile ifaçıkça daha hızlı olduğunu kabul edersiniz ? Şimdi, burada ifanahtarın kullanılmasından daha çok hem 0 hem de 1'in daha hızlı olacağı bir platform örneği var : godbolt.org/z/wcJhvS (Burada da birden fazla optimizasyon olduğunu unutmayın)
th33lf

1
Sayma döngüleri zaten modern superscalar OOO mimarileri üzerinde çalışmıyor. :-) Hafızadan gelen yükler yanlış tahmin edilen dallardan daha yavaş olmayacaktır, bu yüzden soru, dalın ne kadar öngörülebileceğidir? Bu soru, açık ififadelerle veya derleyici tarafından otomatik olarak oluşturulmuş olsun, her türlü koşullu dal için geçerlidir . Ben bir ARM uzmanı değilim, bu yüzden switchhızlı olmakla ilgili iddiaların doğru olduğundan emin değilim if. Yanlış tahmin edilen şubelerin cezasına ve aslında hangi ARM'ye bağlı olacaktır .
Cody Gray

0

Olası bir gerekçe, düşük değerlerin ( numörneğin her zaman 0) daha olası olması durumunda, ilk kod için oluşturulan kodun daha hızlı olabileceğidir. Anahtar için oluşturulan kod tüm değerler için eşit zaman alır.

Bu tabloya göre en iyi vakaların karşılaştırılması . Tablonun açıklaması için bu cevaba bakınız .

Eğer num == 0"if" için xor varsa, test edin, je (atlama ile), ret. Gecikme: 1 + 1 + atlama. Bununla birlikte, xor ve test bağımsızdır, bu nedenle gerçek yürütme hızı 1 + 1 devirden daha yüksek olacaktır.

Eğer num < 7"anahtar" için mov, cmp, ja (atlama olmadan), mov, ret. Gecikme: 2 + 1 + atlama yok + 2.

Atlama ile sonuçlanmayan atlama talimatı, atlama ile sonuçlanandan daha hızlıdır. Ancak, tablo bir sıçrama için gecikmeyi tanımlamaz, bu yüzden hangisinin daha iyi olduğu açık değildir. Sonuncusunun her zaman daha iyi olması ve GCC'nin onu optimize edememesi mümkündür.


1
Hmm, ilginç teori, ancak ifs vs switch için: xor, test, jmp vs mov, cmp jmp. Her biri üç adım olmak üzere üç yönerge atlamadır. En iyi durumda eşit görünüyor, değil mi?
chacham15

3
Msgstr "Atlama ile sonuçlanmayan atlama talimatı, atlama ile sonuçlanandan daha hızlı." Önemli olan dal tahmini.
geza
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.