C derleyicileri neden anahtarı optimize eder ve farklıysa


9

Son zamanlarda garip bir sorunla karşılaştığımda kişisel bir proje üzerinde çalışıyordum.

Çok sıkı bir döngüde 0 ve 15 arasında bir değere sahip bir tamsayı var. 0, 1, 8 ve 9 değerleri için -1 ve 4, 5, 12 ve 13 değerleri için 1 almam gerekiyor.

Birkaç seçeneği kontrol etmek için godbolt'a döndüm ve derleyicinin bir if deyimi ile aynı şekilde bir anahtar deyimini optimize edemediğine şaşırdım.

Bağlantı burada: https://godbolt.org/z/WYVBFl

Kod:

const int lookup[16] = {-1, -1, 0, 0, 1, 1, 0, 0, -1, -1, 0, 0, 1, 1, 0, 0};

int a(int num) {
    return lookup[num & 0xF];
}

int b(int num) {
    num &= 0xF;

    if (num == 0 || num == 1 || num == 8 || num == 9) 
        return -1;

    if (num == 4 || num == 5 || num == 12 || num == 13)
        return 1;

    return 0;
}

int c(int num) {
    num &= 0xF;
    switch (num) {
        case 0: case 1: case 8: case 9: 
            return -1;
        case 4: case 5: case 12: case 13:
            return 1;
        default:
            return 0;
    }
}

B ve c'nin aynı sonuçları vereceğini düşünürdüm ve çözümüm (anahtar ifadesi - başka bir formda) oldukça yavaş olduğu için kendimi verimli bir uygulama bulmak için bit-hack'leri okuyabileceğimi umuyordum.

Garip bir şekilde, bbit hack'lerine derlendiğinde cya neredeyse optimize edilmemişti ya da ahedef donanıma bağlı olarak farklı bir duruma indirgenmişti .

Herkes neden bu tutarsızlık olduğunu açıklayabilir mi? Bu sorguyu optimize etmenin 'doğru' yolu nedir?

DÜZENLE:

açıklama

Ben istiyorum anahtar çözümü en hızlı ya da benzer "temiz" çözüm olarak. Ancak makinemdeki optimizasyonlarla derlendiğinde, if çözümü önemli ölçüde daha hızlıdır.

Göstermek için hızlı bir program yazdım ve TIO'nun yerel olarak bulduğum sonuçlarla aynı sonuçları var: Çevrimiçi deneyin!

İle static inlinearama tablosu biraz hızlandırır: çevrimiçi deneyin!


4
Cevabın "Derleyiciler her zaman aklı başında seçim yapmıyor" şeklinde olduğundan şüpheleniyorum. Kodunuzu GCC 8.3.0 ile bir nesneye -O3derledim cve muhtemelen daha kötü bir şey derledi aveya b( ciki koşullu atlama artı birkaç bit manipülasyon, vs sadece bir koşullu atlama ve daha basit bit manip için vardı b), ama yine de madde testleri ile saf madde daha iyi. Burada gerçekten ne istediğini bilmiyorum; Basit gerçek bir optimize derleyici açabilirsiniz olmasıdır herhangi içine bunlardan herhangi eğer o seçer böylece başkalarının ve bu yayın yapmayacağım ne için hiçbir sert ve hızlı kurallar vardır.
ShadowRanger

Benim sorunum, hızlı olması gerekiyor, ancak eğer çözüm aşırı sürdürülebilir değildir. Derleyicinin daha temiz bir çözümü yeterince optimize etmesini sağlamanın bir yolu var mı? Bu durumda neden bunu yapamayacağını kimse açıklayabilir mi?
LambdaBeta

En azından fonksiyonları statik veya daha iyi satırlar halinde tanımlayarak başlayacağım .
wildplasser

@wildplasser hızlandırıyor, ancak ifyine de atıyor switch(garip bir şekilde arama daha da hızlı hale geliyor) [Takip etmek için TIO]
LambdaBeta

@LambdaBeta Bir derleyiciye belirli bir şekilde optimize etmesini söylemenin bir yolu yoktur. Clang ve msvc'nin bunlar için tamamen farklı bir kod oluşturduğunu göreceksiniz. Umursamıyorsanız ve sadece gcc'de en iyi olanı istiyorsanız, o zaman bunu seçin. Derleyici optimizasyonları sezgisel tarama üzerine kuruludur ve bunlar her durumda en uygun çözümü vermez; Ortalama durumda iyi olmaya çalışıyorlar, her durumda optimal değiller.
Kübik

Yanıtlar:


6

Tüm vakaları açık bir şekilde numaralandırırsanız, gcc çok etkilidir:

int c(int num) {
    num &= 0xF;
    switch (num) {
        case 0: case 1: case 8: case 9: 
            return -1;
        case 4: case 5: case 12: case 13:
            return 1;
            case 2: case 3: case 6: case 7: case 10: case 11: case 14: case 15: 
        //default:
            return 0;
    }
}

sadece basit bir dizinli dalda derlenir:

c:
        and     edi, 15
        jmp     [QWORD PTR .L10[0+rdi*8]]
.L10:
        .quad   .L12
        .quad   .L12
        .quad   .L9
        .quad   .L9
        .quad   .L11
        .quad   .L11
        .quad   .L9
        .quad   .L9
        .quad   .L12
etc...

default:Önerilmezse, gcc'nin iç içe şube sürümüne geri döndüğünü unutmayın .


1
@LambdaBeta Cevabımı kabul etmemeyi ve bunu kabul etmeyi düşünmelisiniz, çünkü modern Intel CPU'lar iki paralel dizinli bellek okuma / çevrim yapabilirken, numaramın verimi muhtemelen 1 arama / döngüdür. Flip tarafında, belki de hack'im SSE2 pslld/ psradveya 8 yollu AVX2 eşdeğerleriyle 4 yollu vektörleştirmeye daha uygundur . Çoğu, kodunuzun diğer özelliklerine bağlıdır.
Iwillnotexist Idonotexist

4

C derleyicilerinin özel durumları vardır switch, çünkü programcıların deyimini anlamasını ve sömürmelerini beklerler switch.

Gibi kod:

if (num == 0 || num == 1 || num == 8 || num == 9) 
    return -1;

if (num == 4 || num == 5 || num == 12 || num == 13)
    return 1;

yetkili C kodlayıcıları tarafından gözden geçirilemez; üç ya da dört hakem aynı anda "bu bir olmalı switch!"

C derleyicilerinin ifbir atlama tablosuna dönüştürmek için deyimlerin yapısını analiz etmesine değmez . Bunun koşulları doğru olmalı ve bir grup ififadede mümkün olan varyasyon miktarı astronomiktir. Analiz hem karmaşık olduğunu ve (: "Hayır, biz bu dönüştürmek olamaz gibi muhtemel negatif gelip ifbir karşı s switch").


Biliyorum, bu yüzden anahtarla başladım. Ancak, if çözümü benim durumumda önemli ölçüde daha hızlı. Temel olarak, derleyiciyi anahtar için daha iyi bir çözüm kullanmaya ikna etmenin bir yolu olup olmadığını soruyorum, çünkü ifs'deki deseni bulabildi, ancak anahtarda değil. (Özellikle bu kadar net veya bakımı mümkün olmadığından
ifsleri sevmiyorum

Bu soruyu yapmamın nedeni tam da bu yana kabul edilmedi ancak kabul edilmedi. Ben istiyorum anahtarı kullanmak, ancak benim durumumda çok yavaş, ben kaçınmak istiyorum ifmümkünse eğer.
LambdaBeta

@LambdaBeta: Arama tablosundan kaçınmanın bir nedeni var mı? Bunu yapın staticve ne atadığınızı biraz daha net hale getirmek istiyorsanız, C99 belirlenmiş başlatıcıları kullanın ve açıkça mükemmel.
ShadowRanger

1
Optimize edicinin yapması gereken daha az iş olması için en azından düşük biti atmaya başlarım.
R .. GitHub BUZA YARDIMCI DURDUR

@ShadowRanger Ne yazık ki bu hala daha yavaştır if(bkz. Düzenleme). @R .. Derleyici için tam bitsel çözüm üzerinde çalıştım, şimdilik kullanıyorum. Ne yazık ki benim durumumda bunlar enumçıplak tamsayılar değil , değerlerdir, bu yüzden bitsel hackler çok sürdürülebilir değildir.
LambdaBeta

4

Aşağıdaki kod, arama dalsız, LUT'suz, ~ 3 saat döngüsü, ~ 4 kullanışlı talimat ve ~ 13 baytlık yüksek inlineoranda x86 makine kodunu hesaplayacaktır .

Bu, 2'nin tamamlayıcı tamsayı gösterimine bağlıdır.

Bununla birlikte, u32ve s32typedefs öğelerinin gerçekten 32 bit işaretsiz ve imzalı tam sayı türlerini gösterdiğinden emin olmalısınız . stdint.htürleri uint32_tve int32_tuygun olurdu ama başlık sizin için kullanılabilir olup olmadığı hakkında hiçbir fikrim yok.

const int lookup[16] = {-1, -1, 0, 0, 1, 1, 0, 0, -1, -1, 0, 0, 1, 1, 0, 0};

int a(int num) {
    return lookup[num & 0xF];
}


int d(int num){
    typedef unsigned int u32;
    typedef signed   int s32;

    // const int lookup[16]     = {-1, -1, 0, 0, 1, 1, 0, 0, -1, -1, 0, 0, 1, 1, 0, 0};
    // 2-bit signed 2's complement: 11 11 00 00 01 01 00 00 11 11 00 00 01 01 00 00
    // Hexadecimal:                   F     0     5     0     F     0     5     0
    const u32 K = 0xF050F050U;

    return (s32)(K<<(num+num)) >> 30;
}

int main(void){
    for(int i=0;i<16;i++){
        if(a(i) != d(i)){
            return !0;
        }
    }
    return 0;
}

Burada kendiniz görün: https://godbolt.org/z/AcJWWf


Sabit seçiminde

Aramanız -1 ve +1 dahil 16 çok küçük sabit içindir. Her biri 2 bit içine sığar ve bunlardan 16 tanesi vardır;

// const int lookup[16]     = {-1, -1, 0, 0, 1, 1, 0, 0, -1, -1, 0, 0, 1, 1, 0, 0};
// 2-bit signed 2's complement: 11 11 00 00 01 01 00 00 11 11 00 00 01 01 00 00
// Hexadecimal:                   F     0     5     0     F     0     5     0
u32 K = 0xF050F050U;

Bunları en anlamlı bite en yakın bit 0'a yerleştirerek, tek bir kaydırma, 2*num2 bitlik numaranızın işaret bitini kaydın işaret bitine yerleştirecektir. 2 bitlik sayıyı 32-2 = 30 bit ile sağa kaydırmak inthileyi tamamlayarak işareti tam olarak genişletir .


Bu, onu magicnasıl yeniden oluşturacağınızı açıklayan bir yorumla yapmanın en temiz yolu olabilir . Nasıl geldiğini açıklayabilir misin?
LambdaBeta

Bu hızlı olduğu için 'temiz' yapılabileceğinden kabul edildi. (bazı önişlemci büyüsü yoluyla :) < xkcd.com/541 >)
LambdaBeta

1
Şubesiz girişimimi yener:!!(12336 & (1<<x))-!!(771 & (1<<x));
technosaurus

0

Aynı efekti yalnızca aritmetik kullanarak oluşturabilirsiniz:

// produces : -1 -1 0 0 1 1 0 0 -1 -1 0 0 1 1 0 0 ...
int foo ( int x )
{
    return 1 - ( 3 & ( 0x46 >> ( x & 6 ) ) );
}

Rağmen, teknik olarak, bu hala (bitsel) bir arama.

Yukarıdakiler çok gizli görünüyorsa, şunları da yapabilirsiniz:

int foo ( int x )
{
    int const y = x & 6;
    return (y == 4) - !y;
}
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.