Tam 8192 öğenin üzerinde döngü yaparken programım neden yavaş?


755

İşte söz konusu programdan alıntı. Matris img[][], SIZE × SIZE boyutundadır ve şu konumda başlatılır:

img[j][i] = 2 * j + i

Daha sonra bir matris yaparsınız res[][]ve buradaki her alan img matrisindeki 9 alanın ortalaması olacak şekilde yapılır. Basitlik için kenarlık 0'da bırakılmıştır.

for(i=1;i<SIZE-1;i++) 
    for(j=1;j<SIZE-1;j++) {
        res[j][i]=0;
        for(k=-1;k<2;k++) 
            for(l=-1;l<2;l++) 
                res[j][i] += img[j+l][i+k];
        res[j][i] /= 9;
}

Programda hepsi bu kadar. Tamlık uğruna, işte daha önce gelenler. Sonra hiçbir kod gelmez. Gördüğünüz gibi, sadece başlatma.

#define SIZE 8192
float img[SIZE][SIZE]; // input image
float res[SIZE][SIZE]; //result of mean filter
int i,j,k,l;
for(i=0;i<SIZE;i++) 
    for(j=0;j<SIZE;j++) 
        img[j][i] = (2*j+i)%8196;

Temel olarak, SIZE 2048'in katları olduğunda bu program yavaştır, örn. Yürütme süreleri:

SIZE = 8191: 3.44 secs
SIZE = 8192: 7.20 secs
SIZE = 8193: 3.18 secs

Derleyici GCC'dir. Bildiğim kadarıyla, bu bellek yönetimi nedeniyle, ama bu konu hakkında çok fazla şey bilmiyorum, bu yüzden burada soruyorum.

Ayrıca bunun nasıl düzeltileceği iyi olurdu, ancak birisi bu yürütme sürelerini açıklayabilirse, zaten yeterince mutlu olurdum.

Ben zaten malloc / free biliyorum, ama sorun kullanılan bellek miktarı değil, sadece yürütme zamanı, bu yüzden bu nasıl yardımcı olacağını bilmiyorum.


67
@bokan, boyut önbelleğin kritik adımının katı olduğu zaman olur.
Luchian Grigore

5
@Mysticial, önemli değil, aynı problemi ortaya koyuyor; kodu farklı olabilir, ancak temel olarak sorunun her ikisi de aynı zamanı sorar (ve başlıkları kesinlikle benzerdir).
Griwes

33
Yüksek performans istiyorsanız görüntüyü 2 boyutlu dizi kullanarak işlememelisiniz. Tüm piksellerin ham olduğunu düşünün ve bunları tek boyutlu dizi gibi işleyin. Bu bulanıklığı iki geçişte yapın. Önce 3 piksel kayar toplam kullanarak çevreleyen piksellerin değerini ekleyin: slideSum + = src [i + 1] -src [i-1]; hedef [i] = slideSum ;. Sonra aynısını dikey olarak yapın ve aynı anda bölün: dest [i] = (src [i-width] + src [i] + src [i + width]) / 9. www-personal.engin.umd.umich.edu/~jwvm/ece581/18_RankedF.pdf
bokan

8
Aslında burada iki şey oluyor. Sadece süper hizalanma değil.
Gizemli

7
(Cevabınızda sadece küçük bir nitpick. İlk kod segmenti için, tüm döngülerinizde parantez olması iyi olurdu.)
Trevor Boyd Smith

Yanıtlar:


954

Fark, aşağıdaki ilgili sorulardaki aynı süper hizalama sorunundan kaynaklanmaktadır:

Ancak bunun nedeni kodda başka bir sorun olması.

Orijinal döngüden başlayarak:

for(i=1;i<SIZE-1;i++) 
    for(j=1;j<SIZE-1;j++) {
        res[j][i]=0;
        for(k=-1;k<2;k++) 
            for(l=-1;l<2;l++) 
                res[j][i] += img[j+l][i+k];
        res[j][i] /= 9;
}

İlk önce iki iç halkanın önemsiz olduğuna dikkat edin. Bunlar aşağıdaki gibi açılabilir:

for(i=1;i<SIZE-1;i++) {
    for(j=1;j<SIZE-1;j++) {
        res[j][i]=0;
        res[j][i] += img[j-1][i-1];
        res[j][i] += img[j  ][i-1];
        res[j][i] += img[j+1][i-1];
        res[j][i] += img[j-1][i  ];
        res[j][i] += img[j  ][i  ];
        res[j][i] += img[j+1][i  ];
        res[j][i] += img[j-1][i+1];
        res[j][i] += img[j  ][i+1];
        res[j][i] += img[j+1][i+1];
        res[j][i] /= 9;
    }
}

Bu, ilgilendiğimiz iki dış döngüyü bırakır.

Şimdi sorunun bu soruda aynı olduğunu görebiliyoruz: Döngülerin sırası bir 2B dizi üzerinden yineleme yaparken neden performansı etkiliyor?

Satır yerine bilge sütunu yineliyorsunuz.


Bu sorunu çözmek için iki döngüyü değiştirmelisiniz.

for(j=1;j<SIZE-1;j++) {
    for(i=1;i<SIZE-1;i++) {
        res[j][i]=0;
        res[j][i] += img[j-1][i-1];
        res[j][i] += img[j  ][i-1];
        res[j][i] += img[j+1][i-1];
        res[j][i] += img[j-1][i  ];
        res[j][i] += img[j  ][i  ];
        res[j][i] += img[j+1][i  ];
        res[j][i] += img[j-1][i+1];
        res[j][i] += img[j  ][i+1];
        res[j][i] += img[j+1][i+1];
        res[j][i] /= 9;
    }
}

Bu, sıralı olmayan tüm erişimi tamamen ortadan kaldırır, böylece artık ikinin büyük güçlerinde rastgele yavaşlamalar elde edemezsiniz.


3,5 GHz'de çekirdek i7 920

Orijinal kod:

8191: 1.499 seconds
8192: 2.122 seconds
8193: 1.582 seconds

Değiştirilmiş Dış-Döngüler:

8191: 0.376 seconds
8192: 0.357 seconds
8193: 0.351 seconds

217
Ayrıca, iç döngülerin açılmasının performans üzerinde bir etkisi olmadığını da not edeceğim. Derleyici muhtemelen otomatik olarak yapar. Dış döngülerdeki problemi daha kolay tespit etmek için onlardan kurtulmak için onları açtım.
Gizemli

29
Ve her bir satırdaki toplamları önbelleğe alarak bu kodu üç kat daha hızlandırabilirsiniz. Ancak bu ve diğer optimizasyonlar orijinal sorunun kapsamı dışındadır.
Eric Postpischil

34
@ClickUpvote Bu aslında bir donanım (önbellekleme) sorunudur. Bunun dil ile ilgisi yok. Yerel koda derleyen veya JIT'leri derleyen başka bir dilde denediyseniz, muhtemelen aynı efektleri görürsünüz.
Mysticial

19
@ClickUpvote: Çok yanlış yönlendirilmiş görünüyorsunuz. Bu "ikinci döngü" sadece mistikleri iç halkaları elle açıyordu. Bu, derleyicinizin neredeyse kesinlikle yapacak bir şeydir ve Mystical bunu sadece dış döngülerle ilgili sorunu daha belirgin hale getirmek için yaptı. Hiçbir şekilde kendinizi yapmaktan rahatsız olmamanız gerekir.
Lily Ballard

154
BU, SO ile ilgili iyi bir cevabın mükemmel bir örneğidir: Benzer sorulara referans verir, ona nasıl yaklaştığınızı adım adım açıklar, sorunu açıklar, sorunu nasıl DÜZELTECEĞİNİZİ, harika biçimlendirmeyi ve hatta kodun bir örneğini açıklar makinenizde. Katkınız için teşekkürler.
MattSayar

57

Aşağıdaki testler, varsayılan Qt Creator yüklemesi tarafından kullanıldığından Visual C ++ derleyicisi ile yapılmıştır (tahmin bayrağı olmadan sanırım). GCC'yi kullanırken, Mystical'in sürümü ile "optimize edilmiş" kodum arasında büyük bir fark yoktur. Sonuç olarak, derleyici optimizasyonları mikro optimizasyonu insanlardan daha iyi hallediyor. Cevabımın geri kalanını referans olarak bırakıyorum.


Görüntüleri bu şekilde işlemek etkili değildir. Tek boyutlu diziler kullanmak daha iyidir. Tüm piksellerin işlenmesi bir döngüde yapılır. Noktalara rastgele erişim aşağıdakiler kullanılarak yapılabilir:

pointer + (x + y*width)*(sizeOfOnePixel)

Bu özel durumda, üç piksel grubunun toplamını yatay olarak hesaplamak ve önbelleğe almak daha iyidir, çünkü her biri üç kez kullanılır.

Bazı testler yaptım ve sanırım paylaşmaya değer. Her sonuç ortalama beş testtir.

User1615209 tarafından orijinal kod:

8193: 4392 ms
8192: 9570 ms

Mistik sürüm:

8193: 2393 ms
8192: 2190 ms

1D dizisi kullanılarak iki geçiş: yatay toplamlar için ilk geçiş, dikey toplam ve ikinci için ikinci geçiş. Üç işaretçi ile iki geçiş adresleme ve sadece bunun gibi artışlar:

imgPointer1 = &avg1[0][0];
imgPointer2 = &avg1[0][SIZE];
imgPointer3 = &avg1[0][SIZE+SIZE];

for(i=SIZE;i<totalSize-SIZE;i++){
    resPointer[i]=(*(imgPointer1++)+*(imgPointer2++)+*(imgPointer3++))/9;
}

8193: 938 ms
8192: 974 ms

1D dizisi kullanarak ve bu şekilde adresleme yapan iki geçiş:

for(i=SIZE;i<totalSize-SIZE;i++){
    resPointer[i]=(hsumPointer[i-SIZE]+hsumPointer[i]+hsumPointer[i+SIZE])/9;
}

8193: 932 ms
8192: 925 ms

Bir geçiş önbellekleme yatay bir satır önde toplam böylece önbellekte kalır:

// Horizontal sums for the first two lines
for(i=1;i<SIZE*2;i++){
    hsumPointer[i]=imgPointer[i-1]+imgPointer[i]+imgPointer[i+1];
}
// Rest of the computation
for(;i<totalSize;i++){
    // Compute horizontal sum for next line
    hsumPointer[i]=imgPointer[i-1]+imgPointer[i]+imgPointer[i+1];
    // Final result
    resPointer[i-SIZE]=(hsumPointer[i-SIZE-SIZE]+hsumPointer[i-SIZE]+hsumPointer[i])/9;
}

8193: 599 ms
8192: 652 ms

Sonuç:

  • Birkaç işaretçi ve sadece artışlar kullanmanın faydaları yok (daha hızlı olacağını düşündüm)
  • Yatay toplamları önbelleğe almak, bunları birkaç kez hesaplamaktan daha iyidir.
  • İki geçiş üç kat daha hızlı değildir, sadece iki kat daha hızlıdır.
  • Hem tek bir geçiş kullanarak hem de bir ara sonucu önbelleğe alarak 3,6 kat daha hızlı elde etmek mümkündür

Eminim daha iyisini yapmak mümkündür.

NOT Lütfen, bu yanıtı Mistik'in mükemmel cevabında açıklanan önbellek problemi yerine genel performans sorunlarını hedeflediğime dikkat edin. Başlangıçta sadece sahte koddu. Yorumlarda testler yapmam istendi ... İşte testlerle tamamen yeniden düzenlenmiş bir versiyon.


9
"Sanırım en az 3 kat daha hızlı" - bu iddiayı bazı metriklerle veya alıntılarla yedeklemeyi umursuyor musunuz?
Adam Rosenfield

8
@AdamRosenfield "Sanırım" = varsayım! = "Öyle" = iddia. Bunun için bir ölçütüm yok ve bir test görmek istiyorum. Ancak benimki 7 artış, 2 alt, 2 ekleme ve piksel başına bir bölme gerektirir. CPU'da kayıtlı olandan daha az yerel var kullanan her döngü. Diğeri, derleyici optimizasyonuna bağlı olarak adresleme için 7 artış, 6 azalma, 1 div ve 10 ila 20 mul gerektirir. Ayrıca döngüdeki her komut önceki talimatın sonucunu gerektirir, bu Pentiumların süper skaler mimarisinin faydalarını ortadan kaldırır. Bu yüzden daha hızlı olmalı.
bokan

3
Orijinal sorunun cevabı tamamen bellek ve önbellek efektleri ile ilgilidir. OP'nin kodunun çok yavaş olmasının nedeni, bellek erişim modelinin çok zayıf önbellek referansı olan satırlar yerine sütunlarla gitmesidir. Bu var özellikle o ardışık satırlar düşük ilişkilendirilebilirlik ile doğrudan eşlemeli önbellek veya önbellekte aynı önbellek hatlarını kullanarak sonuna çünkü önbellek bayan oranı daha da yüksektir bu nedenle, 8192 de kötü. Döngülerin değiştirilmesi, önbellek yerini büyük oranda artırarak muazzam bir performans artışı sağlar.
Adam Rosenfield

1
Aferin, bunlar etkileyici sayılar. Bulduğunuz gibi, her şey bellek performansı ile ilgilidir - artışlarla birkaç işaretçi kullanmak herhangi bir fayda sağlamaz.
Adam Rosenfield

2
@AdamRosenfield Bu sabah oldukça endişelendim çünkü testleri tekrarlayamadım. Performans artışı yalnızca Visual C ++ derleyicisi ile olduğu görülmektedir. Gcc kullanarak, sadece küçük bir fark vardır.
bokan
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.