it-swarm.dev

Döngülerin sırası neden 2B diziyi yinelerken performansı etkiliyor?

Aşağıda, i ve j değişkenlerini değiştirdiğimden hemen hemen aynı olan iki program var. Her ikisi de farklı zamanlarda çalışırlar. Birisi bunun neden olduğunu açıklayabilir mi?

Versiyon 1

#include <stdio.h>
#include <stdlib.h>

main () {
  int i,j;
  static int x[4000][4000];
  for (i = 0; i < 4000; i++) {
    for (j = 0; j < 4000; j++) {
      x[j][i] = i + j; }
  }
}

Versiyon 2

#include <stdio.h>
#include <stdlib.h>

main () {
  int i,j;
  static int x[4000][4000];
  for (j = 0; j < 4000; j++) {
     for (i = 0; i < 4000; i++) {
       x[j][i] = i + j; }
   }
}
344
Mark

Diğerlerinin de söylediği gibi, sorun dizideki bellek konumuna ait depo: x[i][j]. İşte biraz fikir var neden:

2 boyutlu bir diziniz var ama bilgisayardaki bellek doğal olarak 1 boyutlu. Böylece dizinizi şöyle hayal ederken:

0,0 | 0,1 | 0,2 | 0,3
----+-----+-----+----
1,0 | 1,1 | 1,2 | 1,3
----+-----+-----+----
2,0 | 2,1 | 2,2 | 2,3

Bilgisayarınız hafızada tek bir satır olarak saklar:

0,0 | 0,1 | 0,2 | 0,3 | 1,0 | 1,1 | 1,2 | 1,3 | 2,0 | 2,1 | 2,2 | 2,3

2. örnekte, diziye ilk önce 2. sayıyı geçerek erişirsiniz, yani:

x[0][0] 
        x[0][1]
                x[0][2]
                        x[0][3]
                                x[1][0] etc...

Yani hepsini sırayla vuruyorsun. Şimdi ilk sürüme bak. Yapıyoruz:

x[0][0]
                                x[1][0]
                                                                x[2][0]
        x[0][1]
                                        x[1][1] etc...

C'nin bellekte 2-d dizisini ortaya koymasından dolayı, her yere atlamasını istiyorsun. Ama şimdi vuruşçu için: Bu neden önemli? Tüm hafıza erişimi aynı, değil mi?

Hayır: önbellek yüzünden. Hafızanızdaki veriler küçük parçalarda ('önbellek satırları'), genellikle 64 baytta CPU'ya aktarılır. 4 bayt tamsayıya sahipseniz, temiz, küçük bir pakette art arda 16 tam sayı elde edersiniz. Bu bellek parçalarını almak oldukça yavaş; CPU'nuz, tek bir önbellek hattının yüklenmesi için gereken sürede çok fazla iş yapabilir.

Şimdi giriş sırasına bakın: İkinci örnek (1) 16 bitlik bir yığın kapma, (2) hepsini değiştirme, (3) 4000 * 4000/16 kez tekrarlama. Bu Güzel ve hızlı ve CPU'nun daima üzerinde çalışacak bir şeyi var.

İlk örnek (1), 16 inçlik bir yığın kapmak, (2) bunlardan sadece birini değiştirmek, (3) 4000 x 4000 kez tekrarlamak. Bu, bellekten "getirme" sayısının 16 katını gerektirecek. İşlemciniz, o hafızanın ortaya çıkmasını beklemek için oturup zaman geçirmek zorunda kalacak ve bu sırada otururken değerli zamanınızı boşa harcıyorsunuz.

Önemli Not:

Şimdi cevabını aldığına göre, işte ilginç bir not: ikinci örneğinin hızlı olması için doğal bir sebep yok. Mesela Fortran'da ilk örnek hızlı, ikincisi yavaş olacaktır. Bunun nedeni, işleri C'nin yaptığı gibi kavramsal "satırlara" genişletmek yerine, Fortran'ın "sütunlara" yayılmasıdır, yani:

0,0 | 1,0 | 2,0 | 0,1 | 1,1 | 2,1 | 0,2 | 1,2 | 2,2 | 0,3 | 1,3 | 2,3

C'nin düzenine 'satır ana' ve Fortran'lara 'ana sütun' denir. Gördüğünüz gibi, programlama dilinizin satır büyüklüğü mü yoksa sütun büyüklüğü mü olduğunu bilmek çok önemlidir! İşte daha fazla bilgi için bir link: http://en.wikipedia.org/wiki/Row-major_order

574
Robert Martin

Meclis ile ilgisi yok. Bunun nedeni önbellek özlüyor .

C çok boyutlu diziler en son olarak en son boyutta saklanır. Böylece ilk sürüm her yinelemede önbelleği kaçıracak, ikinci sürüm ise bu süreyi değiştirmeyecek. Bu yüzden ikinci versiyonun daha hızlı olması gerekiyor.

Ayrıca bakınız: http://en.wikipedia.org/wiki/Loop_interchange .

66

Sürüm 2, bilgisayarınızın önbelleğini sürüm 1'den daha iyi kullandığı için çok daha hızlı çalışacaktır. Bunu düşünüyorsanız, diziler yalnızca bitişik bellek alanlarıdır. Bir dizideki bir öğeyi talep ettiğinizde, işletim sisteminiz muhtemelen bu öğeyi içeren önbelleğe bir bellek sayfasını getirecektir. Ancak, sonraki birkaç öğe de o sayfada bulunduğundan (bitişik olduklarından), bir sonraki erişim zaten önbellekte olacaktır! Bu, sürüm 2'nin hızlanmasını sağlamak için yaptığı şeydir.

Diğer yandan Sürüm 1, öğelerin sütununa bilge erişiyor ve satır bilge değil. Bu tür bir erişim bellek düzeyinde bitişik değildir, bu nedenle program işletim sistemi önbelleğe alma işleminden bu kadar yararlanamaz.

22
Oleksi

Nedeni önbellek yerel veri erişimidir. İkinci programda, önbelleğe alma ve ön taramadan yararlanan bellekten doğrusal olarak tarama yapıyorsunuz. İlk programınızın bellek kullanım şekli çok daha yayıldı ve bu nedenle daha kötü önbellek davranışına sahip.

12

Önbellek isabetlerine dair diğer mükemmel cevapların yanı sıra, olası bir optimizasyon farkı da var. İkinci döngünüzün derleyici tarafından aşağıdakilerle eşdeğer bir şeyde optimize edilmesi muhtemeldir:

  for (j=0; j<4000; j++) {
    int *p = x[j];
    for (i=0; i<4000; i++) {
      *p++ = i+j;
    }
  }

Bu, ilk döngü için daha düşük bir ihtimaldir, çünkü "p" işaretçisini her seferinde 4000 ile arttırması gerekir.

EDIT:p++ ve hatta *p++ = .. çoğu CPU'nun tek bir CPU komutunda derlenebilir. *p = ..; p += 4000 yapamaz, bu yüzden onu optimize etmenin daha az faydası var. Ayrıca daha zordur, çünkü derleyicinin iç dizinin boyutunu bilmesi ve kullanması gerekir. Ve genellikle normal koddaki iç döngüde meydana gelmez (yalnızca son boyut indeksinin döngüde sabit tutulduğu ve ikinciden ikincisinin kademeli olduğu çok boyutlu diziler için gerçekleşir), bu nedenle optimizasyon öncelikten daha düşüktür .

10
fishinear

Bu çizgi suçlu:

x[j][i]=i+j;

İkinci versiyonda sürekli bellek kullanılıyor, bu yüzden önemli ölçüde daha hızlı olacak.

Denedim

x[50000][50000];

ve yürütme süresi sürüm 1 için 13 saniye, sürüm 2 için 0,6 saniyedir.

7
Nicolas Modrzyk

Genel bir cevap vermeye çalışıyorum.

Çünkü i[y][x], C'deki *(i + y*array_width + x) için bir kısa yoldur (int P[3]; 0[P] = 0xBEEF; klasesini deneyin.).

y üzerinde iterasyon yaptığınız zaman, array_width * sizeof(array_element) boyutundaki parçalar üzerinde yinelersiniz. İç döngüde buna sahipseniz, o parçaların üzerinde array_width * array_height yineleme olacaktır.

Siparişi çevirerek, yalnızca array_height öbek yineleme olacak ve herhangi bir öbek yineleme arasında, yalnızca sizeof(array_element) öğesinin array_width yinelemesine sahip olacaksınız.

Gerçekten eski x86-CPU'larda bu çok önemli değildi, günümüzde x86 çok fazla ön hazırlık ve veri önbelleklemesi yapıyor. Muhtemelen daha yavaş yineleme sıranızda birçok önbellek özlüyor üretirsiniz.

4
Sebastian Mach