Döngü açma ne zaman hala yararlıdır?


94

Döngü açarak son derece performans açısından kritik bazı kodları (bir monte carlo simülasyonunda milyonlarca kez denilen hızlı sıralama algoritması) optimize etmeye çalışıyorum. İşte hızlandırmaya çalıştığım iç döngü:

// Search for elements to swap.
while(myArray[++index1] < pivot) {}
while(pivot < myArray[--index2]) {}

Şunun gibi bir şeye geçmeyi denedim:

while(true) {
    if(myArray[++index1] < pivot) break;
    if(myArray[++index1] < pivot) break;
    // More unrolling
}


while(true) {
    if(pivot < myArray[--index2]) break;
    if(pivot < myArray[--index2]) break;
    // More unrolling
}

Bu kesinlikle bir fark yaratmadı, ben de onu daha okunaklı hale getirdim. Döngü açmayı denediğim diğer zamanlarda da benzer deneyimler yaşadım. Modern donanımdaki branş tahmin edicilerinin kalitesi düşünüldüğünde, döngü açma işlemi ne zaman yararlı bir optimizasyon olur?


1
Neden standart kitaplık hızlı sıralama rutinlerini kullanmadığınızı sorabilir miyim?
Peter Alexander

16
@Poita: Çünkü benimki, yaptığım istatistiksel hesaplamalar için ihtiyacım olan bazı ekstra özelliklere sahip ve kullanım durumlarıma göre çok iyi ayarlanmış ve bu nedenle daha az genel ancak standart lib'den ölçülebilir derecede daha hızlı. Eski bir berbat optimize ediciye sahip olan D programlama dilini kullanıyorum ve büyük rastgele kayan diziler için, yine de GCC'nin C ++ STL sıralamasını% 10-20 oranında geçiyorum.
dsimcha

Yanıtlar:


125

Bağımlılık zincirlerini kırabilirseniz döngü açma mantıklıdır. Bu, bozuk veya süper skaler bir CPU'ya işleri daha iyi planlama ve böylece daha hızlı çalışma imkanı verir.

Basit bir örnek:

for (int i=0; i<n; i++)
{
  sum += data[i];
}

Burada argümanların bağımlılık zinciri çok kısadır. Veri dizisinde bir önbellek kaybınız olduğu için bir durma yaşarsanız, işlemci beklemekten başka bir şey yapamaz.

Öte yandan bu kod:

for (int i=0; i<n; i+=4)
{
  sum1 += data[i+0];
  sum2 += data[i+1];
  sum3 += data[i+2];
  sum4 += data[i+3];
}
sum = sum1 + sum2 + sum3 + sum4;

daha hızlı koşabilir. Bir hesaplamada bir önbelleği kaçırırsanız veya başka bir durakla karşılaşırsanız, durmaya bağlı olmayan başka üç bağımlılık zinciri vardır. Arızalı bir CPU bunları çalıştırabilir.


2
Teşekkürler. Kütüphanede meblağları ve diğer şeyleri hesapladığım başka yerlerde bu tarzda döngü açmayı denedim ve bu yerlerde harikalar yaratıyor. Neredeyse eminim nedeni, önerdiğiniz gibi talimat seviyesi paralelliğini arttırmasıdır.
dsimcha

2
Güzel cevap ve öğretici örnek. Önbellek kaçırmalarındaki duraklamaların bu özel örnek için performansı nasıl etkileyebileceğini görmüyorum . İki kod parçası arasındaki performans farklılıklarını (makinemde ikinci kod parçası 2-3 kat daha hızlıdır) ilkinin kayan nokta şeritlerinde her türlü talimat düzeyinde paralelliği devre dışı bıraktığını belirterek kendime açıklamaya geldim. İkincisi, bir süper skaler CPU'nun aynı anda dört kayan nokta toplamasını yürütmesine izin verir.
Toby Brull

2
Bu şekilde bir toplamı hesaplarken sonucun orijinal döngü ile sayısal olarak aynı olmayacağını unutmayın.
Barabas

Döngü-taşınan bağımlılık bir döngüdür , ekleme. Bir OoO çekirdeği yeterli olacaktır. Burada açma işlemi, kayan nokta SIMD'ye yardımcı olabilir, ancak bu OoO ile ilgili değildir.
Veedrac

2
@Nils: Çok değil; ana akım x86 OoO CPU'lar hala Core2 / Nehalem / K10 ile yeterince benzer. Bir önbellek kaybından sonra yetişmek hala oldukça küçüktü, FP gecikmesini gizlemek hala en büyük avantajdı. 2010'da, saat başına 2 yükleme yapabilen CPU'lar daha da nadirdi (sadece AMD çünkü SnB henüz piyasaya sürülmedi), bu nedenle birden fazla akümülatör, tamsayı kod için şimdiye göre kesinlikle daha az değerliydi (elbette bu, otomatik vektörleştirmesi gereken skaler koddur. böylece derleyiciler vektör elemanları içine veya çoklu birden çok akümülatörleri dönecek olmadığını kim bilir vektör aküler ...)
Peter Cordes

26

Bunlar herhangi bir fark yaratmaz çünkü aynı sayıda karşılaştırma yapıyorsunuz. İşte daha iyi bir örnek. Onun yerine:

for (int i=0; i<200; i++) {
  doStuff();
}

yazmak:

for (int i=0; i<50; i++) {
  doStuff();
  doStuff();
  doStuff();
  doStuff();
}

O zaman bile neredeyse kesinlikle önemli olmayacak, ancak şu anda 200 yerine 50 karşılaştırma yapıyorsunuz (karşılaştırmanın daha karmaşık olduğunu hayal edin).

Bununla birlikte, manuel döngü açma genel olarak büyük ölçüde tarihin bir eseridir. Bu, önemli olduğunda iyi bir derleyicinin sizin için yapacağı şeylerin büyüyen bir başka listesidir. Örneğin, çoğu insan yazmaya zahmet etmeyin x <<= 1veya x += xyerine x *= 2. Siz sadece yazın x *= 2ve derleyici onu sizin için en iyi olana göre optimize eder.

Temel olarak, derleyicinizi ikinci kez tahmin etmeye giderek daha az ihtiyaç duyulmaktadır.


1
@Mike Şaşırdığınızda iyi bir fikir varsa kesinlikle optimizasyonu kapatmak, ancak Poita_'nın gönderdiği bağlantıyı okumaya değer. Derleyiciler bu işte acı verici derecede iyi hale geliyor.
dmckee --- eski moderatör yavru kedi

17
@Mike "Böyle şeyleri ne zaman yapıp yapmayacağıma mükemmel bir şekilde karar verebiliyorum" ... Süper insan değilseniz, şüpheliyim.
Bay Boy

5
@John: Bunu neden söylediğini bilmiyorum; insanlar optimizasyonun bir tür siyah sanat olduğunu düşünüyor, sadece derleyiciler ve iyi tahminçiler nasıl yapılacağını biliyor. Her şey talimatlara, döngülere ve neden harcandıklarına bağlıdır. SO'da defalarca açıkladığım gibi, bunların nasıl ve neden harcandığını söylemek çok kolay. Zamanın önemli bir yüzdesini kullanması gereken bir döngüm varsa ve içeriğe kıyasla döngü ek yükünde çok fazla döngü harcıyorsa, bunu görebilir ve onu açabilirim. Kod kaldırma için aynı. Bir dahi gerektirmez.
Mike Dunlavey

3
Eminim o kadar da zor değildir, ancak yine de bunu derleyici kadar hızlı yapabileceğinizden şüpheliyim. Derleyicinin bunu sizin için yapmasındaki sorun nedir? Beğenmezseniz, optimizasyonları kapatın ve zamanınızı 1990 gibi harcayın!
Bay Boy

2
Döngü açmadan kaynaklanan performans kazancının, kaydettiğiniz karşılaştırmalarla hiçbir ilgisi yoktur. Hiçbir şey.
bobbogo

14

Modern donanımdaki dal tahminine bakılmaksızın, çoğu derleyici yine de sizin için döngü açma işlemi yapar.

Derleyicinizin sizin için ne kadar optimizasyon yaptığını öğrenmek faydalı olacaktır.

Felix von Leitner'ın sunumunu konu hakkında çok aydınlatıcı buldum . Okumanızı tavsiye ederim. Özet: Modern derleyiciler ÇOK akıllıdır, bu nedenle el optimizasyonları neredeyse hiçbir zaman etkili değildir.


7
Bu iyi bir okuma, ancak işaret üzerinde düşündüğüm tek kısım, veri yapısını basit tutmaktan bahsettiği yerdi. Ne yürütülmekte olan, - Gerisi dev yersiz varsayımıyla doğru ama dayanakları oldu sahiptir olmak. Yaptığım ayarlamada, büyük miktarda zaman soyutlama kodunun gereksiz dağlarına girerken, yazmaçlar ve önbellek kaçırmalarından endişe duyan insanları buluyorum.
Mike Dunlavey

4
"el optimizasyonları neredeyse hiçbir zaman etkili değildir" → Göreve tamamen yeniyseniz, belki de doğrudur. Aksi takdirde doğru değil.
Veedrac

1
2019'da hala derleyicinin otomatik girişimlerine göre önemli kazançlar sağlayan manuel kayıt iptalleri yaptım .. bu yüzden derleyicinin hepsini yapmasına izin vermek o kadar da güvenilir değil. O kadar sık ​​açılmıyor gibi görünüyor. En azından c # için tüm diller adına konuşamıyorum.
WDUK

2

Anladığım kadarıyla, modern derleyiciler halihazırda uygun yerlerde döngüleri açıyor - bir örnek gcc, optimizasyon bayraklarını geçerse kılavuzda şunu söylüyor:

Derleme zamanında veya döngüye girildiğinde yineleme sayısı belirlenebilen döngüleri geri alma.

Dolayısıyla, pratikte derleyicinizin sizin için önemsiz durumları yapması muhtemeldir. Bu nedenle, derleyicinin kaç tane yinelemeye ihtiyaç duyacağını belirlemesi için döngülerinizden mümkün olduğunca çoğunun kolay olduğundan emin olmak size kalmıştır.


Tam zamanında derleyiciler genellikle döngü açma yapmazlar, buluşsal yöntemler çok pahalıdır. Statik derleyiciler bunun üzerinde daha fazla zaman harcayabilir, ancak iki baskın yol arasındaki fark önemlidir.
Abel

2

Döngü açma, ister elle açma isterse derleyiciyi açma olsun, özellikle daha yeni x86 CPU'larda (Core 2, Core i7) genellikle ters yönde verimli olabilir. Alt satır: Kodunuzu, bu kodu dağıtmayı planladığınız CPU'lar üzerinde döngü açarak ve döngü olmadan kıyaslayın.


Neden özellikle recet x86 CPU'larda?
JohnTortugo

7
@JohnTortugo: Modern x86 CPU'ları küçük döngüler için belirli optimizasyonlara sahiptir - örneğin Çekirdek ve Nehalem mimarilerinde Döngü Akış Dedektörü - LSD önbelleğine sığacak kadar küçük olmaması için bir döngüyü açmak bu optimizasyonu bozar. Örneğin tomshardware.com/reviews/Intel-i7-nehalem-cpu,2041-3.html
Paul R

1

Bilmeden denemek bunu yapmanın yolu değildir.
Bu sıralama, toplam sürenin yüksek bir yüzdesini mi alıyor?

Döngü açmanın tüm yaptığı, durma koşulu için karşılaştırma ve atlama, artan / azalan döngü ek yükünü azaltmaktır. Döngüde yaptığınız şey, döngünün kendisinden daha fazla talimat döngüsü gerektiriyorsa, yüzde olarak çok fazla gelişme görmeyeceksiniz.

İşte maksimum performansın nasıl alınacağına dair bir örnek.


1

Döngü açma, belirli durumlarda yardımcı olabilir. Tek kazanç bazı testleri atlamak değil!

Örneğin, skaler değiştirmeye, yazılımın önceden getirilmesinin verimli bir şekilde eklenmesine izin verebilir ... Agresif bir şekilde kaydırarak bunun ne kadar yararlı olabileceğini (-O3 ile bile çoğu döngüde kolayca% 10 hızlanma elde edebilirsiniz) gerçekten şaşıracaksınız.

Yine de daha önce söylendiği gibi, bu döngüye çok bağlıdır ve derleyici ve deney gereklidir. Bir kural yapmak zordur (veya listeyi açmak için derleyici buluşsal yöntemi mükemmel olacaktır)


0

Döngü açma tamamen probleminizin boyutuna bağlıdır. Tamamen algoritmanızın boyutu daha küçük çalışma gruplarına indirebilmesine bağlıdır. Yukarıda yaptığın şey öyle görünmüyor. Bir monte carlo simülasyonunun açılıp kapanmayacağından bile emin değilim.

Döngü açma için iyi bir senaryo bir görüntüyü döndürmek olacaktır. Ayrı çalışma gruplarını döndürebildiğiniz için. Bunun işe yaraması için yineleme sayısını azaltmanız gerekir.


Simülasyonun ana döngüsünden değil, simülasyonumun iç döngüsünden çağrılan hızlı bir sıralamayı açıyordum.
dsimcha

0

Döngüde ve döngüde çok sayıda yerel değişken varsa, döngü açma hala yararlıdır. Döngü indeksi için bir tane kaydetmek yerine bu kayıtları daha fazla yeniden kullanmak.

Örneğinizde, kayıtları aşırı kullanmak yerine az miktarda yerel değişken kullanıyorsunuz.

Karşılaştırma (döngü sonuna kadar) test, özellikle de harici bir işleve bağlıysa, karşılaştırma ağırsa (yani talimatsız ) önemli bir dezavantajdır .

Döngü açma, CPU'nun dal tahmini için farkındalığını artırmaya da yardımcı olur, ancak bunlar yine de olur.

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.