GCC 5.4.0 ile pahalı bir sıçrama


171

Ben (sadece önemli kısmı gösteren) böyle görünüyordu bir işlevi vardı:

double CompareShifted(const std::vector<uint16_t>& l, const std::vector<uint16_t> &curr, int shift, int shiftY)  {
...
  for(std::size_t i=std::max(0,-shift);i<max;i++) {
     if ((curr[i] < 479) && (l[i + shift] < 479)) {
       nontopOverlap++;
     }
     ...
  }
...
}

Bu şekilde yazıldığında, işlev makinemde ~ 34ms aldı. Bool çarpım koşulunu değiştirdikten sonra (kodun böyle görünmesi):

double CompareShifted(const std::vector<uint16_t>& l, const std::vector<uint16_t> &curr, int shift, int shiftY)  {
...
  for(std::size_t i=std::max(0,-shift);i<max;i++) {
     if ((curr[i] < 479) * (l[i + shift] < 479)) {
       nontopOverlap++;
     }
     ...
  }
...
}

yürütme süresi ~ 19 ms'ye düştü.

Kullanılan derleyici -O3 ile GCC 5.4.0 idi ve godbolt.org kullanılarak oluşturulan asm kodunu kontrol ettikten sonra, ilk örneğin bir sıçrama oluşturduğunu, ikincisinin ise bir sıçrama oluşturduğunu öğrendim. İlk örneği kullanırken bir atlama talimatı oluşturan GCC 6.2.0'ı denemeye karar verdim, ancak GCC 7 artık bir tane oluşturmuyor gibi görünüyor.

Kodu hızlandırmak için bu yolu bulmak oldukça korkunçtu ve biraz zaman aldı. Derleyici neden bu şekilde davranıyor? Bu, programcıların dikkat etmesi gereken bir şey mi? Buna benzer başka şeyler var mı?

DÜZENLEME: godbolt bağlantısı https://godbolt.org/g/5lKPF3


17
Derleyici neden bu şekilde davranıyor? Oluşturulan kod doğru olduğu sürece derleyici istediği gibi yapabilir. Bazı derleyiciler optimizasyonda diğerlerinden daha iyidir.
Jabberwocky

26
Benim tahminim, bunun kısa devre değerlendirmesi buna &&neden oluyor.
Jens

9
Bu yüzden bizde de var &.
rubenvb

7
@ Jakub sıralama büyük olasılıkla yürütme hızını artıracak, bu soruya bakın .
rubenvb

8
@rubenvb "değerlendirilecek olmamalıdır" değil aslında yok demek hiçbir yan etkisi yoktur ifadesi için her şeyi. Vektörün sınır kontrolü yaptığını ve GCC'nin sınırların dışında olmayacağını kanıtlayamadığından şüpheleniyorum. DÜZENLEME: Aslında değil mi edilir i aut olmaktan + shift durdurmak için bir şey yapıyor.
Random832

Yanıtlar:


263

Mantıksal AND operatörü ( &&) kısa devre değerlendirmesini kullanır, yani ikinci test sadece ilk karşılaştırma doğru olarak değerlendirilirse yapılır. Bu genellikle tam olarak ihtiyaç duyduğunuz anlambilimdir. Örneğin, aşağıdaki kodu göz önünde bulundurun:

if ((p != nullptr) && (p->first > 0))

İşaretini kaldırmadan önce işaretçinin boş olmadığından emin olmalısınız. Bu takdirde değildi kısa devre değerlendirme bir boş gösterici dereferencing olurdum, çünkü tanımsız davranış olurdu.

Kısa devre değerlendirmesinin, koşulların değerlendirmesinin pahalı bir süreç olduğu durumlarda bir performans kazancı sağlaması da mümkündür. Örneğin:

if ((DoLengthyCheck1(p) && (DoLengthyCheck2(p))

Eğer DoLengthyCheck1başarısız çağırarak hiçbir anlamı yoktur DoLengthyCheck2.

Bununla birlikte, sonuçtaki ikili dosyada, kısa devre işlemi genellikle iki dalla sonuçlanır, çünkü bu, derleyicinin bu semantiği korumanın en kolay yoludur. (Bu nedenle, madalyonun diğer tarafında, kısa devre değerlendirmesi bazen optimizasyon potansiyelini engelleyebilir .) Bunu, ifGCC 5.4 tarafından beyanınız için oluşturulan nesne kodunun ilgili bölümüne bakarak görebilirsiniz :

    movzx   r13d, WORD PTR [rbp+rcx*2]
    movzx   eax,  WORD PTR [rbx+rcx*2]

    cmp     r13w, 478         ; (curr[i] < 479)
    ja      .L5

    cmp     ax, 478           ; (l[i + shift] < 479)
    ja      .L5

    add     r8d, 1            ; nontopOverlap++

cmpBurada, her biri ayrı bir koşullu atlama / dal ( javeya yukarıdaysa atlama) izleyen iki karşılaştırmayı ( talimatları) görürsünüz .

Dalların yavaş olması ve bu nedenle sıkı döngülerden kaçınılması genel bir kuraldır. Bu, mütevazi 8088'den (yavaş getirme süreleri ve son derece küçük ön alma kuyruğu [bir talimat önbelleği ile karşılaştırılabilir), neredeyse şube tahmini eksikliğiyle birlikte neredeyse tüm x86 işlemciler için geçerliydi, alınan dalların önbelleğin boşaltılmasını gerektirdiği anlamına geliyordu. ) (uzun boru hatları yanlış tahmin edilen dalları benzer şekilde pahalı hale getiren) modern uygulamalara. Oraya girdiğim küçük uyarıyı not et. Pentium Pro'dan bu yana modern işlemciler, dalların maliyetini en aza indirmek için tasarlanmış gelişmiş dal tahmin motorlarına sahiptir. Dalın yönü doğru bir şekilde tahmin edilebilirse, maliyet minimumdur. Çoğu zaman, bu iyi çalışır, ancak şube öngörücüsünün yanınızda olmadığı patolojik vakalara girerseniz,kodunuz son derece yavaşlayabilir . Dizinizin ayrılmamış olduğunu söylediğiniz için muhtemelen burada olduğunuz yer burasıdır.

Ölçütlerin, &&ile değiştirilmesinin *kodu belirgin şekilde daha hızlı hale getirdiğini doğruladığını söylüyorsunuz . Bunun nedeni, nesne kodunun ilgili kısmını karşılaştırdığımızda belirgindir:

    movzx   r13d, WORD PTR [rbp+rcx*2]
    movzx   eax,  WORD PTR [rbx+rcx*2]

    xor     r15d, r15d        ; (curr[i] < 479)
    cmp     r13w, 478
    setbe   r15b

    xor     r14d, r14d        ; (l[i + shift] < 479)
    cmp     ax, 478
    setbe   r14b

    imul    r14d, r15d        ; meld results of the two comparisons

    cmp     r14d, 1           ; nontopOverlap++
    sbb     r8d, -1

Burada daha fazla talimat olduğundan, bunun daha hızlı olabileceği biraz sezgiseldir , ancak optimizasyon bazen bu şekilde çalışır. cmpBurada aynı karşılaştırmaların ( ) yapıldığını görüyorsunuz , ancak şimdi her birinin önünde bir xorve ardından a geliyor setbe. XOR, bir kaydı silmek için standart bir numaradır. setbeBir bayrağın değerini temel biraz ayarlar ve genellikle şubesiz Kodu uygulamak için kullanılan bir x86 talimatıdır. İşte setbetersi ja. Karşılaştırma eşit veya daha düşükse hedef yazmaçını 1 olarak ayarlar (kayıt önceden sıfırlandığından, aksi takdirde 0 olacaktır), jakarşılaştırma yukarıdaysa dallıdır. Bu iki değer r15bver14bkayıtları ile birlikte çarpılır imul. Çarpma geleneksel olarak nispeten yavaş bir işlemdi, ancak modern işlemcilerde çok hızlıdır ve bu özellikle hızlı olacaktır, çünkü sadece iki bayt büyüklüğünde değerleri çarpmaktadır.

Çarpmayı, &kısa devre değerlendirmesi yapmayan bitsel AND operatörü ( ) ile kolayca değiştirebilirsiniz . Bu, kodu çok daha açık hale getirir ve derleyicilerin genel olarak tanıdığı bir modeldir. Ancak bunu kodunuzla yaptığınızda ve GCC 5.4 ile derlediğinizde, ilk dalı yaymaya devam eder:

    movzx   r13d, WORD PTR [rbp+rcx*2]
    movzx   eax,  WORD PTR [rbx+rcx*2]

    cmp     r13w, 478         ; (curr[i] < 479)
    ja      .L4

    cmp     ax, 478           ; (l[i + shift] < 479)
    setbe   r14b

    cmp     r14d, 1           ; nontopOverlap++
    sbb     r8d, -1

Kodu bu şekilde yayınlaması için teknik bir neden yoktur, ancak bir nedenden ötürü, iç sezgiselliği, bunun daha hızlı olduğunu söylüyor. Bu olur dallanma öngörüsü yanınızda olsaydı muhtemelen daha hızlı olabilir, ama dal tahmini daha sık Başarılı daha başarısız olursa, büyük olasılıkla daha yavaş olacaktır.

Derleyicinin yeni nesilleri (ve Clang gibi diğer derleyiciler) bu kuralı bilir ve bazen el optimizasyonu ile aradığınız kodu oluşturmak için kullanır. Düzenli olarak Clang &&ifadeleri, kullansaydım yayılan kodun aynısını tercüme ediyor &. Aşağıdakiler, normal &&operatörü kullanarak kodunuzla GCC 6.2'den ilgili çıktıdır :

    movzx   r13d, WORD PTR [rbp+rcx*2]
    movzx   eax,  WORD PTR [rbx+rcx*2]

    cmp     r13d, 478         ; (curr[i] < 479)
    jg      .L7

    xor     r14d, r14d        ; (l[i + shift] < 479)
    cmp     eax, 478
    setle   r14b

    add     esi, r14d         ; nontopOverlap++

Ne kadar zeki Not Bu olduğunu! İmzalı koşulları ( jgve setle) imzasız koşulların ( jave setbe) aksine kullanıyor , ancak bu önemli değil. Hala eski sürüm gibi ilk koşul için karşılaştırma ve şube yaptığını setCCve ikinci koşul için dalsız kod oluşturmak için aynı komutu kullandığını , ancak artışın nasıl yapıldığından çok daha verimli hale geldiğini görebilirsiniz. . Bir sbbişlemin bayraklarını ayarlamak için ikinci, yedekli bir karşılaştırma yapmak yerine, r14dbu değeri koşulsuz olarak eklemek için 1 veya 0 olacak bilgileri kullanır nontopOverlap. Eğer r14d0, daha sonra ek no-op; aksi takdirde, tam olması gerektiği gibi 1 ekler.

GCC 6.2 kısa devre operatörünü kullandığınızda aslında bitsel operatöre göre daha verimli kod üretir :&&&

    movzx   r13d, WORD PTR [rbp+rcx*2]
    movzx   eax,  WORD PTR [rbx+rcx*2]

    cmp     r13d, 478         ; (curr[i] < 479)
    jg      .L6

    cmp     eax, 478          ; (l[i + shift] < 479)
    setle   r14b

    cmp     r14b, 1           ; nontopOverlap++
    sbb     esi, -1

Şube ve koşullu küme hala oradadır, ancak şimdi daha az akıllı bir artış yoluna geri dönmektedir nontopOverlap. Bu, derleyicinizi zekice çalıştırmaya çalışırken neden dikkatli olmanız gerektiği konusunda önemli bir derstir!

Ancak , dallanma kodunun gerçekten daha yavaş olduğunu gösteren kriterler ile kanıtlayabilirseniz , derleyicinizi zekice denemek ve ödemek zahmetli olabilir. Bunu sökme işleminin dikkatle incelenmesi ile yapmanız yeterlidir ve derleyicinin daha sonraki bir sürümüne geçtiğinizde kararlarınızı yeniden değerlendirmeye hazır olun. Örneğin, sahip olduğunuz kod şu şekilde yeniden yazılabilir:

nontopOverlap += ((curr[i] < 479) & (l[i + shift] < 479));

Burada hiçbir ififade yok ve derleyicilerin büyük çoğunluğu bunun için dallanma kodu yaymayı asla düşünmeyecek. GCC bir istisna değildir; tüm sürümler aşağıdakine benzer bir şey üretir:

    movzx   r14d, WORD PTR [rbp+rcx*2]
    movzx   eax,  WORD PTR [rbx+rcx*2]

    cmp     r14d, 478         ; (curr[i] < 479)
    setle   r15b

    xor     r13d, r13d        ; (l[i + shift] < 479)
    cmp     eax, 478
    setle   r13b

    and     r13d, r15d        ; meld results of the two comparisons
    add     esi, r13d         ; nontopOverlap++

Önceki örneklerle birlikte takip ediyorsanız, bu size çok tanıdık gelmelidir. Her iki karşılaştırma da dalsız bir şekilde yapılır, ara sonuçlar andbirlikte düzenlenir ve daha sonra bu sonuç (0 veya 1 olacak) ile adddüzenlenir nontopOverlap. Dalsız kod istiyorsanız, bu neredeyse almanızı sağlayacaktır.

GCC 7 daha da akıllı hale geldi. Şimdi, yukarıdaki hile için orijinal kod olarak neredeyse aynı kodu (talimatların hafif bir yeniden düzenlenmesi hariç) üretir. Peki, "Derleyici neden bu şekilde davranıyor?" , muhtemelen mükemmel olmadıkları içindir! Sezgisel yöntemi mümkün olan en uygun kodu oluşturmak için kullanmaya çalışırlar, ancak her zaman en iyi kararları vermezler. Ama en azından zamanla daha akıllı olabilirler!

Bu duruma bakmanın bir yolu, dallanma kodunun daha iyi en iyi performansa sahip olmasıdır. Şube tahmini başarılı olursa, gereksiz işlemleri atlamak biraz daha hızlı çalışma süresine neden olur. Ancak, dalsız kod en iyi durum performansına sahiptir. Şube tahmini başarısız olursa, bir şubeden kaçınmak için gereken birkaç ek talimatın uygulanması kesinlikle yanlış tahmin edilen bir şubeden daha hızlı olacaktır . En zeki ve en zeki derleyiciler bile bu seçimi yapmakta zorlanacak.

Ve bunun programcıların dikkat etmesi gereken bir şey olup olmadığı sorunuz için, mikro optimizasyonlarla hızlandırmaya çalıştığınız bazı sıcak döngüler dışında, cevap neredeyse kesinlikle hayır. Ardından, sökme ile oturun ve ince ayar yapmanın yollarını bulun. Daha önce de söylediğim gibi, derleyicinin daha yeni bir sürümüne güncellediğinizde bu kararları tekrar gözden geçirmeye hazır olun, çünkü ya zor kodunuzla aptalca bir şey yapabilir ya da geri dönebileceğiniz optimizasyon sezgisel yöntemlerini değiştirmiş olabilir. orijinal kodunuzu kullanmak için. İyice yorum yap!


3
Evrensel bir “daha ​​iyi” yok. Her şey sizin durumunuza bağlıdır, bu nedenle bu tür düşük seviye performans optimizasyonu yaparken kesinlikle kıyaslamanız gerekir. Cevabımda açıkladığım gibi, şube tahmininin büyüklüğünü kaybediyorsanız, yanlış tahmin edilen şubeler kodunuzu çok yavaşlatacaktır . Kodun son biti herhangi bir dal kullanmaz ( j*talimatların yokluğuna dikkat edin ), bu durumda daha hızlı olacaktır. [devamı]
Cody Gray

3

2
@ 8bit Bob haklı. Önceden getirme kuyruğundan bahsediyordum. Muhtemelen bir önbellek dememeliydim, ancak ifade konusunda çok endişelenmedim ve tarihi meraktan başka kimsenin umursamadığını anlayamadığım için özellikleri hatırlamak için çok uzun zaman harcamadım. Detaylar istiyorsanız, Michael Abrash'in Meclis Dili Zen değeri paha biçilmezdir. Kitabın tamamı çevrimiçi olarak çeşitli yerlerde mevcuttur; İşte dallanma için geçerli bölüm , ancak önceden getirme ile ilgili bölümleri de okumalı ve anlamalısınız.
Cody Gray

6
@Hurkyl Tüm cevabın bu soruya konuştuğunu hissediyorum. Gerçekten bunu açıkça söylemediğim için haklısın, ama zaten yeterince uzun gibi görünüyordu. :-) Her şeyi okumak için zaman ayıran herkes bu noktayı yeterince anlamalıdır. Ancak bir şeyin eksik olduğunu veya daha fazla açıklığa ihtiyacı olduğunu düşünüyorsanız, lütfen cevabı eklemek için cevabı düzenleme konusunda utanmayın. Bazı insanlar bunu sevmez, ama kesinlikle umursamıyorum. Bu konuda kısa bir yorum ekledim, 8bittree tarafından önerildiği gibi ifadelerimin bir modifikasyonu.
Cody Gray

2
Hah, tamamlayıcı için teşekkürler, @green. Önerecek özel bir şeyim yok. Her şeyde olduğu gibi, yaparak, görerek ve deneyimleyerek uzman olursunuz. X86 mimarisi, optimizasyonu, derleyici içleri ve diğer düşük seviyeli şeyler söz konusu olduğunda elimden alabileceğim her şeyi okudum ve hala bilinmesi gereken her şeyin sadece bir kısmını biliyorum. Öğrenmenin en iyi yolu, ellerinizi kirleterek kirletmektir. Ancak başlamayı ummadan önce, C (veya C ++), işaretçiler, montaj dili ve diğer tüm düşük seviyeli temelleri sağlam bir şekilde kavramanız gerekir.
Cody Gray

23

Dikkat edilmesi gereken önemli bir nokta,

(curr[i] < 479) && (l[i + shift] < 479)

ve

(curr[i] < 479) * (l[i + shift] < 479)

anlamsal olarak eşdeğer değildir! Özellikle, eğer şu durumda bir durum varsa:

  • 0 <= ive i < curr.size()ikisi de doğru
  • curr[i] < 479 yanlış
  • i + shift < 0ya i + shift >= l.size()da doğru

daha sonra, ifadenin (curr[i] < 479) && (l[i + shift] < 479)iyi tanımlanmış bir boole değeri olduğu garanti edilir. Örneğin, bir segmentasyon hatasına neden olmaz.

Bununla birlikte, bu koşullar altında, sentezleme (curr[i] < 479) * (l[i + shift] < 479)olan tanımsız davranış ; o olan bir segment hataya neden izin verdi.

Bu, orijinal kod snippet'i için, örneğin, derleyicinin her iki karşılaştırmayı gerçekleştiren ve bir andişlem gerçekleştiren bir döngü yazamayacağı anlamına gelir, çünkü derleyici de l[i + shift]gerekli olmadığı bir durumda hiçbir zaman bir segfault'a neden olmayacağını kanıtlayamaz .

Kısacası, orijinal kod parçası optimizasyon için ikincisine göre daha az fırsat sunar. (elbette, derleyicinin fırsatı tamamen fark edip etmediğini tamamen farklı bir soru)

Bunun yerine orijinal sürümü düzeltebilirsiniz.

bool t1 = (curr[i] < 479);
bool t2 = (l[i + shift] < 479);
if (t1 && t2) {
    // ...

Bu! Burada shift(ve max) değerine bağlı olarak burada UB var ...
Matthieu M.

18

&&Operatör kısa devre değerlendirme uygular. Bu, ikinci işlenenin yalnızca ilk işlenenin değerlendirilmesi durumunda değerlendirildiği anlamına gelir true. Bu kesinlikle bu durumda bir sıçrama ile sonuçlanır.

Bunu göstermek için küçük bir örnek oluşturabilirsiniz:

#include <iostream>

bool f(int);
bool g(int);

void test(int x, int y)
{
  if ( f(x) && g(x)  )
  {
    std::cout << "ok";
  }
}

Montajcı çıktısını burada bulabilirsiniz .

Oluşturulan kodu ilk aramaları görebilir f(x), sonra çıkışı kontrol eder ve bunun g(x)ne zaman olduğunu değerlendirmeye atlarsınız true. Aksi takdirde işlevi terk eder.

Bunun yerine "boolean" çarpımını kullanmak her iki işlenenin de değerlendirmesini her zaman zorlar ve bu nedenle bir sıçramaya gerek yoktur.

Verilere bağlı olarak, atlama CPU'nun boru hattını ve spekülatif yürütme gibi diğer şeyleri bozduğu için yavaşlamaya neden olabilir. Normalde şube tahmini yardımcı olur, ancak verileriniz rastgele ise tahmin edilebilecek fazla bir şey yoktur.


1
Neden çarpmanın her iki işlenenin de değerlendirmesini her zaman zorladığını söylüyorsunuz? 0 * x = x * 0 = 0, x değerine bakılmaksızın. Optimizasyon olarak, derleyici çarpmayı da "kısa devre" yapabilir. Örneğin, stackoverflow.com/questions/8145894/… adresine bakın . Dahası, &&operatörden farklı olarak, çarpma ya birinci ya da ikinci argümanla tembel olarak değerlendirilebilir ve optimizasyon için daha fazla özgürlük sağlanır.
SomeWittyUsername

@Jens - "Normalde şube tahmini yardımcı olur, ancak verileriniz rastgele ise tahmin edilebilecek çok fazla bir şey yoktur." - güzel cevap verir.
SChepurin

1
@SomeWittyUsername Tamam, derleyici elbette gözlemlenebilir davranışı koruyan herhangi bir optimizasyon yapmakta serbesttir. Bu onu değiştirebilir veya değiştirmeyebilir ve hesaplamaları dışlayabilir. eğer hesaplarsanız 0 * f()ve fgözlemlenebilir davranışa sahipseniz , derleyicinin bunu yapması gerekir. Aradaki fark, kısa devre değerlendirmesinin zorunlu olması, &&ancak eşdeğer olduğunu gösterebilmesi durumunda izin verilmesidir *.
Jens

@SomeWittyUsername yalnızca 0 değerinin bir değişken veya sabitten tahmin edilebildiği durumlarda. Sanırım bu davalar çok az. Dizi erişimi söz konusu olduğundan, kesinlikle OP durumunda optimizasyon yapılamaz.
Diego Sevilla

3
@Jens: Kısa devre değerlendirmesi zorunlu değildir. Kodun sadece kısa devre gibi davranması gerekir ; derleyicinin sonuca ulaşmak için sevdiği her türlü aracı kullanmasına izin verilir.

-2

Bunun nedeni, mantıksal işleci kullandığınızda &&, derleyicinin if ifadesinin başarılı olması için iki koşulu denetlemesi gerektiğidir. Ancak ikinci durumda, int değerini dolaylı olarak boole dönüştürdüğünüz için, derleyici, (muhtemelen) tek bir atlama koşulu ile birlikte geçirilen türlere ve değerlere dayalı olarak bazı varsayımlar yapar. Derleyicinin jmps'leri bit kaydırmalarıyla tamamen optimize etmesi de mümkündür.


8
Atlama, ikinci koşulun yalnızca ilkinin doğru olması durumunda değerlendirilmesinden kaynaklanır . Kod başka türlü değerlendirmemelidir, bu nedenle derleyici bunu daha iyi optimize edemez ve hala doğru olamaz (ilk ifadeyi çıkaramadıkça her zaman doğru olacaktır).
rubenvb
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.