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 DoLengthyCheck1
baş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, if
GCC 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++
cmp
Burada, her biri ayrı bir koşullu atlama / dal ( ja
veya 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. cmp
Burada aynı karşılaştırmaların ( ) yapıldığını görüyorsunuz , ancak şimdi her birinin önünde bir xor
ve ardından a geliyor setbe
. XOR, bir kaydı silmek için standart bir numaradır. setbe
Bir bayrağın değerini temel biraz ayarlar ve genellikle şubesiz Kodu uygulamak için kullanılan bir x86 talimatıdır. İşte setbe
tersi 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), ja
karşılaştırma yukarıdaysa dallıdır. Bu iki değer r15b
ver14b
kayı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ı ( jg
ve setle
) imzasız koşulların ( ja
ve 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ı setCC
ve 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 sbb
işlemin bayraklarını ayarlamak için ikinci, yedekli bir karşılaştırma yapmak yerine, r14d
bu değeri koşulsuz olarak eklemek için 1 veya 0 olacak bilgileri kullanır nontopOverlap
. Eğer r14d
0, 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 if
ifade 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 and
birlikte düzenlenir ve daha sonra bu sonuç (0 veya 1 olacak) ile add
dü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!