Derleyiciler neden burada kaydedilmiş bir kayıt kullanmakta ısrar ediyor?


10

Bu C kodunu düşünün:

void foo(void);

long bar(long x) {
    foo();
    return x;
}

Ben ya GCC 9.3 ya derlemek ya -O3da -Os, bunu elde:

bar:
        push    r12
        mov     r12, rdi
        call    foo
        mov     rax, r12
        pop     r12
        ret

Clang'dan elde edilen çıktı , callee tarafından kaydedilen kayıt rbxyerine seçim dışında aynıdır r12.

Ancak, daha çok benzeyen montaj görmek istiyorum / bekliyoruz:

bar:
        push    rdi
        call    foo
        pop     rax
        ret

İngilizce olarak, işte neler olduğunu görüyorum:

  • Callee tarafından kaydedilen bir kaydın eski değerini yığına itme
  • Taşı xO çağrılan tarafından kaydedilen kayıt içine
  • Aramak foo
  • Taşı xdönüş değeri kayıt içine çağrılan tarafından kaydedilen kasadan
  • Callee tarafından kaydedilen kaydın eski değerini geri yüklemek için yığını açın

Neden callee tarafından kaydedilmiş bir kayıtla uğraşmaya zahmet ediyorsunuz? Bunu neden yapmıyorsunuz? Daha kısa, daha basit ve muhtemelen daha hızlı görünüyor:

  • Yığına xdoğru itin
  • Aramak foo
  • Pop xdönüş değeri yazmacına istiften

Meclisim yanlış mı? Ekstra bir kayıtla uğraşmaktan bir şekilde daha az verimli mi? Her ikisinin de cevabı "hayır" ise, neden GCC veya clang bunu bu şekilde yapmıyor?

Godbolt bağlantısı .


Düzenleme: Değişken anlamlı olarak kullanılsa bile, bu durumu göstermek için daha az önemsiz bir örnek:

long foo(long);

long bar(long x) {
    return foo(x * x) - x;
}

Bunu anladım:

bar:
        push    rbx
        mov     rbx, rdi
        imul    rdi, rdi
        call    foo
        sub     rax, rbx
        pop     rbx
        ret

Şunu tercih ederim:

bar:
        push    rdi
        imul    rdi, rdi
        call    foo
        pop     rdi
        sub     rax, rdi
        ret

Bu kez, sadece iki yönerge dışında iki yönerge, ancak temel kavram aynı.

Godbolt bağlantısı .


4
İlginç kaçırılmış optimizasyon.
fuz

1
büyük olasılıkla, geçirilen parametrenin kullanılacağı varsayımı, böylece bir uçucu kaydı kaydetmek ve bu parametreye daha sonraki erişimler kayıttan daha hızlı olduğundan, geçirilen parametreyi yığın üzerinde olmayan bir kayıtta tutmak istersiniz. x'i foo'ya geçirdiğinizde bunu göreceksiniz. bu nedenle yığın çerçeve kurulumunun genel bir parçasıdır.
old_timer

foo olmadan yığını kullanmadığını görüyorum, bu yüzden evet kaçırılmış bir optimizasyon ama birisinin eklemek, işlevi analiz etmek ve değer kullanılmıyorsa ve bu kayıtla bir çakışma yoksa (genellikle orada dır-dir).
old_timer

kol arka ucu bunu gcc'de de yapar. muhtemelen arka uç değil
old_timer

clang 10 aynı hikaye (kol arka uç).
old_timer

Yanıtlar:


5

TP: DR:

  • Derleyici iç bileşenleri muhtemelen bu optimizasyonu kolayca arayacak şekilde ayarlanmamıştır ve büyük olasılıkla çağrılar arasındaki büyük işlevlerin içinde değil, yalnızca küçük işlevler için yararlıdır.
  • Büyük işlevler oluşturmak için satır içi yapmak çoğu zaman daha iyi bir çözümdür
  • fooRBX'i kaydetmez / geri yüklemezse , gecikme ve işlem hacmi dengesi olabilir .

Derleyiciler karmaşık makine parçalarıdır. Bir insan gibi "akıllı" değiller ve olası her optimizasyonu bulmak için pahalı algoritmalar genellikle ekstra derleme zamanında maliyete değmez.

Ben bu rapor GCC hata 69986 - itmek kullanarak -OS ile mümkün küçük kodu / / yeniden dökmek için pop 2016 yılında geri ; GCC geliştiricileri tarafından herhangi bir etkinlik veya yanıt gelmedi. : /

Biraz ilgili: GCC hatası 70408 - aynı arama korumalı kaydı yeniden kullanmak bazı durumlarda daha küçük kodlar verebilir - derleyici geliştiricileri bana GCC'nin bu optimizasyonu yapabilmesi için çok fazla iş gerektireceğini, çünkü değerlendirme sırasını gerektirdiğini söyledi foo(int)hedeflemeyi daha basit hale getirecek şeye göre iki çağrı.


Eğer foo kaydetmez / geri rbxkendisi, ekstra bir mağaza / yeniden gecikme vs performansı (talimat sayımı) arasında bir tercih söz var x> dönüş_değeri bağımlılık zincirinin -.

Derleyiciler genellikle gecikme işleminden fazla gecikmeyi tercih eder, örneğin imul reg, reg, 10(3 döngü gecikme süresi, 1 / saat işlem hacmi) yerine 2x LEA kullanmak , çünkü kodların çoğu Skylake gibi tipik 4 geniş boru hatlarında saatte 4 uops / saatten önemli ölçüde daha azdır. (Daha fazla talimat / uops ROB'de daha fazla yer kaplıyor, ancak aynı sıra dışı pencerenin ne kadar ileride görebileceğini azaltıyor ve yürütme aslında muhtemelen 4'ten az uops / saat ortalaması.)

Eğer fooyaptığı itme / Rbx pop, daha sonra gecikme için kazanmak için pek bir şey yok. Dönüş adresinde kodu getirmeyi geciktiren retbir retyanlış tahmin veya I-önbellek özledim olmadıkça , geri yüklemenin hemen sonra değil, hemen öncesinde gerçekleşmesi muhtemelen ilgili değildir .

Önemsiz işlevlerin çoğu RBX'i kaydeder / geri yükler, bu nedenle RBX'de bir değişken bırakmanın aslında çağrı boyunca gerçekten bir kayıtta kaldığı anlamına gelmez. (Hangi arama korumalı kayıt işlevlerinin seçildiğini rasgele seçmek bazen bunu hafifletmek için iyi bir fikir olabilir.)


Bu nedenle evet push rdi/ bu durumda pop raxdaha verimli olur ve bu, arayanın kaydedilmesi / geri yüklenmesi için daha fazla talimat ve ekstra depolama / yeniden yükleme gecikmesi arasındaki dengeye ve dengeye bağlı olarak, küçük yaprak olmayan işlevler için kaçırılan bir optimizasyon olabilir .fooxrbx

Yığın çözme meta verilerinin, tıpkı bir yığın yuvasına sub rsp, 8dökülmesi / yeniden yüklenmesi için kullanılmış gibi RSP'deki değişiklikleri temsil etmesi mümkündür x. (Ama derleyiciler kullanmak yerine, ya bu optimizasyon bilmiyorum pushrezerv alanı ve bir değişkeni başlatabilir. Ne C / C ++ derleyicisi yerel değişkenler yaratarak yerine sadece? ESP kez artırma itme pop talimatları kullanabilirsiniz . Ve yapıyor bu aşkın için her yerel basmada .eh_frameyığın işaretçisini ayrı ayrı hareket ettirdiğiniz için bir yerel değişken daha büyük yığın çözme meta verilerine yol açar . Bu da derleyicilerin çağrı korumalı regları kaydetmek / geri yüklemek için push / pop kullanmasını durdurmaz.


Bu optimizasyonu aramak için derleyicilere öğretmeye değer olursa IDK

Belki bir fonksiyon içindeki tek bir çağrıda değil, tüm fonksiyon hakkında iyi bir fikir olabilir. Dediğim gibi, fooyine de RBX'i kurtaracak / geri yükleyecek kötümser varsayımlara dayanıyor . (Veya x'den dönüş değerine kadar olan gecikmenin önemli olmadığını biliyorsanız, işlem hacmi için optimizasyon yapmak önemlidir. Ancak derleyiciler bunu bilmez ve genellikle gecikme süresini optimize eder).

Bu kötümser varsayımı çok sayıda kodla yapmaya başlarsanız (işlevler içindeki tek işlev çağrıları gibi), RBX'in kaydedilmediği / geri yüklenmediği ve avantaj elde edebileceğiniz daha fazla durum almaya başlayacaksınız.

Ayrıca, bu ekstra kaydetme / geri yükleme push / pop'u bir döngüde istemezsiniz, sadece RBX'i döngü dışında kaydedin / geri yükleyin ve işlev çağrıları yapan döngülerde çağrı korumalı kayıtları kullanın. Döngüler olmadan bile, genellikle, çoğu işlev birden çok işlev çağrısı yapar. Bu optimizasyon fikri x, ilk aramadan hemen önce ve sondan sonra aramalardan herhangi birini gerçekten kullanmazsanız geçerli olabilir , aksi takdirde callbir aradan sonra bir pop yapıyorsanız , her biri için 16 bayt yığın hizalamasını koruma konusunda bir sorununuz varsa başka bir çağrıdan önce.

Derleyiciler genel olarak küçük fonksiyonlarda mükemmel değildir. Ancak CPU'lar için de harika değil. Derleyiciler callee'nin iç kısımlarını görüp normalden daha fazla varsayımlarda bulunmadıkça , satır içi olmayan işlev çağrılarının en iyi duruma getirme üzerinde en çok etkisi vardır . Satır içi olmayan bir işlev çağrısı örtük bir bellek engelidir: Arayan kişinin bir işlevin tüm dünyada erişilebilir verileri okuyabileceğini veya yazabileceğini varsayması gerekir, bu nedenle bu tür tüm değişkenlerin C soyut makinesiyle senkronize olması gerekir. (Kaçış analizi, adresleri işlevden kaçmadıysa, yerlilerin çağrılar boyunca kayıtlarda tutulmasına izin verir.) Ayrıca, derleyici, çağrı clobbered kayıtlarının tamamen engellenmiş olduğunu varsaymalıdır. Bu, arama korumalı XMM kayıtları olmayan x86-64 Sistem V'deki kayan noktayı emer.

Küçük fonksiyonlar bar()arayanlara daha iyi gelir. -fltoÇoğu durumda dosya sınırlarında bile olabilmesi için derleyin . (İşlev işaretçileri ve paylaşılan kitaplık sınırları bunu yenebilir.)


Derleyicilerin bu optimizasyonları yapmak için uğraşmalarının bir nedeni , derleyici içlerinde , normal yığından farklı olarak bir sürü farklı kod gerektirmesi , arama korumalı nasıl kaydedileceğini bilen kayıt ayırma kodu gerektirmesidir. kaydeder ve kullanır.

yani, uygulanması gereken çok iş ve sürdürülmesi gereken çok kod olacaktır ve eğer bunu yapmak konusunda aşırı hevesli olursa, daha kötü kod oluşturabilir.

Ve ayrıca (umarım) önemli olmadığı; Bu konularda, sen inlining gerektiğini bar, arayanın içine veya inlining fooiçine bar. Bu, çok farklı barişlevler olmadıkça ve foobüyük olmadığı ve bir nedenden dolayı arayanlara giremeyecekleri sürece sorun değildir.


emin değilim neden bazı derleyici kodu bu şekilde çevirmek sormak var, ne zaman daha iyi kullanmak olabilir .., çeviri hatası değilse. örneğin olası clang neden bu kadar garip (optimize değil) bu döngü tercüme , sormak gcc, icc ve hatta msvc ile karşılaştırmak
RbMm

1
@RbMm: Ne demek istediğini anlamıyorum. Bu, clang için tamamen ayrı bir cevapsız optimizasyona benziyor, bu sorunun ne ile ilgisi yok. Eksik optimizasyon hataları vardır ve çoğu durumda düzeltilmelidir. Devam et ve bugs.llvm.org'da
Peter Cordes

evet, kod örneğim orijinal soru ile ilgisiz. tuhaf (benim bakışım için) çevirinin başka bir örneği (ve sadece tek clang derleyicisi için). ancak sonuç asm kodu yine de doğru. sadece en iyi ve eveen değil yerli karşılaştırmak gcc / icc / msvc
RbMm
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.