Neden işe yaramaz MOV talimatları getirmek x86_64 montajında ​​sıkı bir döngüyü hızlandırır?


222

Arka fon:

Yerleşik montaj dili ile bazı Pascal kodlarını optimize ederken , gereksiz bir MOVtalimat fark ettim ve kaldırdım.

Şaşırtıcı bir şekilde, gereksiz talimatları kaldırmak programımın yavaşlamasına neden oldu .

Rasgele, işe yaramaz MOVtalimatlar eklemenin performansı daha da artırdığını buldum .

Etki düzensizdir ve yürütme sırasına göre değişir: tek bir satır tarafından yukarı veya aşağı aktarılan aynı önemsiz talimatlar yavaşlamaya neden olur .

CPU'nun her türlü optimizasyon ve düzene yapıldığını anlıyorum, ancak bu daha çok kara büyü gibi görünüyor.

Veri:

Kodumun bir sürümü koşullu olarak bir döngü ortasında kez çalışan üç önemsiz işlemleri derler 2**20==1048576. (Çevredeki program sadece SHA-256 karmaları hesaplar ).

Oldukça eski makinemdeki sonuçlar (Intel (R) Core ™) 2 CPU 6400 @ 2.13 GHz):

avg time (ms) with -dJUNKOPS: 1822.84 ms
avg time (ms) without:        1836.44 ms

Programlar bir döngüde 25 kez çalıştırıldı ve çalışma sırası her seferinde rastgele değişti.

Alıntı:

{$asmmode intel}
procedure example_junkop_in_sha256;
  var s1, t2 : uint32;
  begin
    // Here are parts of the SHA-256 algorithm, in Pascal:
    // s0 {r10d} := ror(a, 2) xor ror(a, 13) xor ror(a, 22)
    // s1 {r11d} := ror(e, 6) xor ror(e, 11) xor ror(e, 25)
    // Here is how I translated them (side by side to show symmetry):
  asm
    MOV r8d, a                 ; MOV r9d, e
    ROR r8d, 2                 ; ROR r9d, 6
    MOV r10d, r8d              ; MOV r11d, r9d
    ROR r8d, 11    {13 total}  ; ROR r9d, 5     {11 total}
    XOR r10d, r8d              ; XOR r11d, r9d
    ROR r8d, 9     {22 total}  ; ROR r9d, 14    {25 total}
    XOR r10d, r8d              ; XOR r11d, r9d

    // Here is the extraneous operation that I removed, causing a speedup
    // s1 is the uint32 variable declared at the start of the Pascal code.
    //
    // I had cleaned up the code, so I no longer needed this variable, and 
    // could just leave the value sitting in the r11d register until I needed
    // it again later.
    //
    // Since copying to RAM seemed like a waste, I removed the instruction, 
    // only to discover that the code ran slower without it.
    {$IFDEF JUNKOPS}
    MOV s1,  r11d
    {$ENDIF}

    // The next part of the code just moves on to another part of SHA-256,
    // maj { r12d } := (a and b) xor (a and c) xor (b and c)
    mov r8d,  a
    mov r9d,  b
    mov r13d, r9d // Set aside a copy of b
    and r9d,  r8d

    mov r12d, c
    and r8d, r12d  { a and c }
    xor r9d, r8d

    and r12d, r13d { c and b }
    xor r12d, r9d

    // Copying the calculated value to the same s1 variable is another speedup.
    // As far as I can tell, it doesn't actually matter what register is copied,
    // but moving this line up or down makes a huge difference.
    {$IFDEF JUNKOPS}
    MOV s1,  r9d // after mov r12d, c
    {$ENDIF}

    // And here is where the two calculated values above are actually used:
    // T2 {r12d} := S0 {r10d} + Maj {r12d};
    ADD r12d, r10d
    MOV T2, r12d

  end
end;

Kendin dene:

Kendiniz denemek istiyorsanız kod GitHub'da çevrimiçi .

Sorularım:

  • Neden bir kaydedicinin içeriğini RAM'a gereksiz yere kopyalamak performansı arttırır?
  • Neden aynı işe yaramaz talimat bazı satırlarda hızlanma ve diğerlerinde yavaşlama sağlar?
  • Bu davranış, bir derleyici tarafından tahmin edilebilir şekilde kullanılabilecek bir şey mi?

7
Aslında bağımlılık zincirlerini kırmak için hizmet edebilir 'yararsız' talimatlar her türlü vb emekli gibi fiziksel kayıtlar bu işlemleri Kötüye Kullanmak biraz bilgi gerektirir vardır işaretlemek vardır mikromimarisine . Sorunuz, insanları github'a yönlendirmek yerine, asgari bir örnek olarak kısa bir talimat dizisi sağlamalıdır.
Brett Hale

1
@BrettHale iyi bir nokta, teşekkürler. Bazı yorumlardan bir kod alıntısı ekledim. Bir kaydın değerini ram'a kopyalamak, içindeki değer daha sonra kullanılsa bile kaydı emekli olarak işaretler mi?
teğet fırtınası

9
Bu ortalamalara standart sapmayı koyabilir misiniz? Bu yazıda gerçek bir fark olduğuna dair gerçek bir gösterge yok.
starwed

2
Lütfen rdtscp komutunu kullanarak talimatları zamanlamayı deneyebilir ve her iki sürüm için de saat döngülerini kontrol edebilir misiniz?
jakobbotsch

2
Bellek hizalaması nedeniyle de olabilir mi? Matematiği kendim yapmadım (tembel: P) ama bazı kukla talimatlar eklemek kodunuzun hafızaya hizalanmasına neden olabilir ...
Lorenzo Dematté

Yanıtlar:


144

Hız artışının en olası nedeni şudur:

  • MOV takılması, sonraki talimatları farklı bellek adreslerine kaydırır
  • taşınan talimatlardan biri önemli bir koşullu daldı
  • o şube, şube tahmin tablosundaki diğer ad nedeniyle yanlış tahmin ediliyordu
  • dalın taşınması takma adı ortadan kaldırmış ve dalın doğru tahmin edilmesini sağlamıştır

Core2'niz her koşullu atlama için ayrı bir geçmiş kaydı tutmaz. Bunun yerine, tüm koşullu sıçramaların ortak bir geçmişini tutar. Küresel dal tahmininin bir dezavantajı, eğer farklı koşullu sıçramalar ilişkisiz ise tarihin alakasız bilgilerle seyreltilmesidir.

Bu küçük dal tahmini öğreticisi , dal tahmin arabelleklerinin nasıl çalıştığını gösterir. Önbellek, dal komutunun adresinin alt kısmı tarafından dizine eklenir. İki önemli ilişkisiz dal aynı alt bitleri paylaşmadığı sürece bu işe yarar. Bu durumda, birçok yanlış tahmin edilen dallara (talimat boru hattını durduran ve programınızı yavaşlatan) neden olan bir örtüşme ile sonuçlanırsınız.

Şube yanlış tahminlerinin performansı nasıl etkilediğini anlamak istiyorsanız, şu mükemmel cevaba bir göz atın: https://stackoverflow.com/a/11227902/1001643

Derleyiciler genellikle hangi dalların takma adı olacağını ve bu takma adların önemli olup olmayacağını bilmek için yeterli bilgiye sahip değildir. Ancak, bu bilgi çalışma zamanında Cachegrind ve VTune gibi araçlarla belirlenebilir .


2
Hmm. Kulağa umut verici geliyor. Bu sha256 uygulamasındaki tek koşullu dallar FOR döngülerinin sonunu kontrol eder. O zaman bu revizyonu git'teki bir tuhaflık olarak etiketledim ve optimizasyona devam ettim. Bir sonraki adımımdan biri, pascal FOR döngüsünü montajda kendim yeniden yazmaktı, bu noktada bu ek talimatların artık olumlu bir etkisi olmadı. Belki de ücretsiz pascal'ın ürettiği kod, işlemci için tahmin ettiğim basit sayaçtan daha zordu.
tangentstorm

1
@tangentstorm Kulağa iyi bir özet gibi geliyor. Şube tahmin tablosu çok büyük değil, bu nedenle bir tablo girişi birden fazla dalı ifade edebilir. Bu, bazı tahminleri işe yaramaz hale getirebilir. Çakışan dallardan biri tablonun başka bir bölümüne hareket ederse sorun kolayca giderilir. Hemen hemen her küçük değişiklik bunu yapabilir :-)
Raymond Hettinger

1
Bu gözlemlediğim belirli davranışın en makul açıklaması olduğunu düşünüyorum, bu yüzden bunu cevap olarak işaretleyeceğim. Teşekkürler. :)
tangentstorm

3
Bochs'a katkıda bulunanlardan birinin karşılaştığı benzer bir sorun hakkında kesinlikle mükemmel bir tartışma var, bunu cevabınıza eklemek isteyebilirsiniz: emulators.com/docs/nx25_nostradamus.htm
leander

3
Insn hizalaması, şube hedeflerinden çok daha fazlası için önemlidir. Kod çözme darboğazları Core2 ve Nehalem için büyük bir sorundur: genellikle yürütme birimlerini meşgul etmekte zorlanır. Sandybridge'in uop önbelleğini tanıtması, ön uç verimini büyük miktarda artırdı. Şube hedeflerinin hizalanması bu sorun nedeniyle yapılır , ancak tüm kodları etkiler.
Peter Cordes

80

Http://research.google.com/pubs/pub37077.html adresini okumak isteyebilirsiniz.

TL; DR: programlara rastgele nop talimatları eklemek performansı kolayca% 5 veya daha fazla artırabilir ve hayır, derleyiciler bundan kolayca yararlanamaz. Genellikle dal tahmincisi ve önbellek davranışının bir birleşimidir, ancak örneğin bir rezervasyon istasyonu duraklaması da olabilir (kırık veya bağımlı kaynak aşırı abonelikleri olmayan bir bağımlılık zinciri olmasa bile).


1
İlginç. Ancak işlemci (veya FPC) bu durumda ram'a yazmanın bir NOP olduğunu görecek kadar akıllı mı?
tangentstorm

8
Assembler optimize edilmemiştir.
Marco van de Voort

5
Derleyiciler, tekrar tekrar oluşturma ve profilleme gibi inanılmaz pahalı optimizasyonlar yaparak ve daha sonra derleyici çıktısını simüle edilmiş bir tavlama veya genetik algoritma ile değiştirerek yararlanabilirler. Bu alandaki bazı çalışmaları okudum. Ancak derlemek için en az% 5-10 dakikalık% 100 CPU'dan bahsediyoruz ve sonuçta elde edilen optimizasyonlar muhtemelen CPU çekirdek modeli ve hatta çekirdek veya mikrokod revizyonuna özgü olacak.
AdamIerymenko

Buna rastgele NOP demezdim, NOP'lerin performans üzerinde neden olumlu bir etkisi olabileceğini açıklıyorlar (tl; dr: stackoverflow.com/a/5901856/357198 ) ve NOP'un rastgele yerleştirilmesi performansın düşmesine neden oldu. Çalışmanın ilginç yanı, 'stratejik' NOP'un GCC tarafından kaldırılmasının genel performans üzerinde hiçbir etkisi olmamasıdır!
PuercoPop

15

Modern CPU'lara montaj talimatlarına inanıyorum, bir CPU'ya yürütme talimatları sağlamak için bir programcıya son görünür katman olurken, aslında CPU tarafından gerçek yürütmeden birkaç katman.

Modern CPU'lar, CISC x86 talimatlarını davranışta daha fazla RISC olan dahili talimatlara dönüştüren RISC / CISC hibritleridir. Ek olarak, komutları daha büyük eşzamanlı çalışma grupları halinde ( VLIW / Itanium titanic gibi) gruplamaya çalışan sıra dışı yürütme analizörleri, dal tahminörleri, Intel'in "mikro-ops füzyonu" vardır . Kodun tanrı bilir için daha hızlı çalışmasını sağlayacak önbellek sınırları bile vardır - neden daha büyükse (belki de önbellek denetleyicisi daha akıllıca yerleştirir veya daha uzun süre tutar).

CISC'nin her zaman bir montajdan mikrokodere çeviri katmanı vardır, ancak asıl nokta modern CPU'larda işlerin çok daha karmaşık olmasıdır. Modern yarı iletken üretim tesislerindeki tüm ekstra transistör gayrimenkulleriyle, CPU'lar muhtemelen paralel olarak birkaç optimizasyon yaklaşımı uygulayabilir ve daha sonra en iyi hızlanmayı sağlayanı seçebilir. Ek talimatlar, CPU'yu diğerlerinden daha iyi bir optimizasyon yolu kullanmaya yönlendiriyor olabilir.

Ek talimatların etkisi muhtemelen CPU modeline / üretimine / üreticisine bağlıdır ve öngörülebilir olması muhtemel değildir. Montaj dilini bu şekilde optimize etmek, belki de CPU'ya özgü yürütme yollarını kullanarak birçok CPU mimarisi nesline karşı yürütmeyi gerektirecektir ve montaj yapıyorsanız, muhtemelen bunu zaten biliyor olsanız bile, gerçekten gerçekten önemli kod bölümleri için arzu edilir olacaktır.


6
Cevabınız biraz kafa karıştırıcı. Birçok yerde, söylediğiniz şeylerin çoğu doğru olsa da, tahmin ettiğiniz gibi görünüyor.
alcuadrado

2
Belki açıklığa kavuşturmalıyım. Şaşırtıcı bulduğum şey kesinliğin olmaması
alcuadrado

3
mantıklı ve iyi argümanlarla tahmin etmek tamamen geçerlidir.
jturolla

7
Intel'de özel teşhis ekipmanlarına erişimi olan bir mühendis olmadıkça, OP'nin neden bu garip davranışı gözlemlediğini kimse kesin olarak bilemez. Yani diğerlerinin yapabileceği tahmin. Bu @ cowarldlydragon'ın hatası değil.
Alex D

2
downvote; Söylediklerinizin hiçbiri OP'nin gördüğü davranışı açıklamaz. Cevabınız işe yaramaz.
fuz

0

Önbelleği hazırlama

İşlemleri belleğe taşıma önbelleği hazırlayabilir ve sonraki taşıma işlemlerini daha hızlı yapabilir. Bir CPU genellikle iki yük birimine ve bir depolama birimine sahiptir. Bir yük birimi bellekten bir kayıt defterine (döngü başına bir okuma) okuyabilir, bir depolama birimi kayıttan belleğe depolar. Kayıtlar arasında işlem yapan başka birimler de vardır. Tüm birimler paralel olarak çalışır. Bu nedenle, her döngüde aynı anda birkaç işlem yapabiliriz, ancak ikiden fazla yük, bir mağaza ve birkaç kayıt işlemi gerçekleştiremeyiz. Genellikle düz kayıtlarla 4 adede kadar basit işlem, XMM / YMM kayıtlarıyla 3 adede kadar basit işlem ve her türlü kayıtla 1-2 karmaşık işlemdir. Kodunuzda yazmaçlarla çok sayıda işlem vardır, bu nedenle bir sahte bellek deposu işlemi ücretsizdir (zaten 4'ten fazla kayıt işlemi olduğu için), ancak bellek önbelleğini sonraki depolama işlemi için hazırlar. Bellek depolarının nasıl çalıştığını öğrenmek için lütfenIntel 64 ve IA-32 Mimarileri Optimizasyon Referans Kılavuzu .

Yanlış bağımlılıkları kırmak

Her ne kadar bu tam olarak sizin durumunuzu göstermiyor olsa da, bazen 64 bit işlemci (sizin durumunuzda olduğu gibi) altında 32 bit mov işlemleri kullanmak, yüksek bitleri (32-63) temizlemek ve bağımlılık zincirlerini kırmak için kullanılır.

X86-64 altında, 32-bit işlenenlerin kullanılmasının 64-bit kaydının daha yüksek bitlerini temizlediği iyi bilinmektedir. Intel® 64 ve IA-32 Mimarileri Yazılım Geliştirici Kılavuzu Cilt 1'in ilgili bölümünü - 3.4.1.1 - okuyun :

32-bit işlenenler 32-bit bir sonuç üretir, hedef genel amaçlı kayıtta sıfırdan 64-bit bir sonuca genişletilir

Yani, ilk bakışta işe yaramayabilecek gibi görünen mov talimatları, uygun kayıtların daha yüksek bitlerini temizler. Bize ne veriyor? Bağımlılık zincirlerini kırar ve 1995'te Pentium Pro'dan bu yana CPU'lar tarafından dahili olarak uygulanan Sipariş Dışı algoritmasıyla talimatların rasgele sırada paralel olarak yürütülmesine izin verir .

A alıntı Intel® 64 ve Manuel IA-32 Mimarileri Optimizasyon Referans Bölüm 3.5.1.8:

Kısmi kaydı değiştiren kod dizileri bağımlılık zincirinde bir miktar gecikme yaşayabilir, ancak bağımlılık kırma deyimleri kullanılarak önlenebilir. Intel Core mikro mimarisine dayanan işlemcilerde, yazılım kayıt içeriğini sıfıra temizlemek için bu talimatları kullandığında, bir dizi talimat yürütme bağımlılığının temizlenmesine yardımcı olabilir. Kısmi kayıtlar yerine 32 bit kayıtlarda çalışarak talimatlar arasındaki kayıt bölümlerine olan bağımlılıkları ortadan kaldırın. Hareketler için, bu 32-bit hareketlerle veya MOVZX kullanılarak yapılabilir.

Derleme / Derleyici Kodlama Kural 37. (M etkisi, MH genelliği) : Kısmi kayıtlar yerine 32 bit kayıtlarda çalışarak talimatlar arasındaki kayıt bölümlerine olan bağımlılıkları kırın. Hareketler için, bu 32-bit hareketlerle veya MOVZX kullanılarak yapılabilir.

X64 için 32-bit işlenenlere sahip MOVZX ve MOV eşdeğerdir - hepsi kopma bağımlılığı zincirleridir.

Bu yüzden kodunuz daha hızlı çalışır. Bağımlılık yoksa, CPU ilk bakışta ikinci komut ilk komut tarafından kullanılan bir kaydı değiştirmiş gibi görünse de ve ikisi paralel olarak yürütülemez gibi görünse de, kayıtları dahili olarak yeniden adlandırabilir. Ancak kayıt yeniden adlandırma nedeniyle yapabilirler.

Yeniden adlandırma kaydı , CPU tarafından dahili olarak kullanılan ve aralarında gerçek veri bağımlılığı olmayan ardışık talimatlarla kayıtların yeniden kullanılmasından kaynaklanan yanlış veri bağımlılıklarını ortadan kaldıran bir tekniktir.

Sanırım şimdi çok açık olduğunu görüyorsunuz.


Tüm bunlar doğrudur, ancak soruda sunulan kodla ilgisi yoktur.
Cody Gray

@CodyGray - geri bildiriminiz için teşekkür ederiz. Cevabı düzenledim ve vaka hakkında bir bölüm ekledim - kayıt işlemleriyle çevrili belleğe mov önbelleği hazırlıyor ve mağaza birimi boşta olduğu için ücretsiz. Böylece sonraki mağaza işlemi daha hızlı olacaktır.
Maxim Masiutin

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.