Elementwise ilaveler neden ayrı döngülerde bir birleşik döngüden daha hızlıdır?


2246

Varsayalım a1, b1, c1ve d1yığın bellek ve benim sayısal koda noktası aşağıdaki temel halka vardır.

const int n = 100000;

for (int j = 0; j < n; j++) {
    a1[j] += b1[j];
    c1[j] += d1[j];
}

Bu döngü başka bir dış fordöngü yoluyla 10.000 kez yürütülür . Hızlandırmak için kodu şu şekilde değiştirdim:

for (int j = 0; j < n; j++) {
    a1[j] += b1[j];
}

for (int j = 0; j < n; j++) {
    c1[j] += d1[j];
}

Tam iyileştirme ve Intel Core 2 Duo (x64) üzerinde 32-bit için SSE2 etkinleştirilmiş MS Visual C ++ 10.0'da derlenen ilk örnek 5.5 saniye alır ve çift döngü örneği yalnızca 1.9 saniye sürer. Sorum şu: (Lütfen alt kısımdaki yeniden yazılmış soruma bakın)

PS: Emin değilim, eğer bu yardımcı olur:

İlk halkanın sökülmesi temelde şöyle görünür (bu blok tam programda yaklaşık beş kez tekrarlanır):

movsd       xmm0,mmword ptr [edx+18h]
addsd       xmm0,mmword ptr [ecx+20h]
movsd       mmword ptr [ecx+20h],xmm0
movsd       xmm0,mmword ptr [esi+10h]
addsd       xmm0,mmword ptr [eax+30h]
movsd       mmword ptr [eax+30h],xmm0
movsd       xmm0,mmword ptr [edx+20h]
addsd       xmm0,mmword ptr [ecx+28h]
movsd       mmword ptr [ecx+28h],xmm0
movsd       xmm0,mmword ptr [esi+18h]
addsd       xmm0,mmword ptr [eax+38h]

Çift döngü örneğinin her döngüsü bu kodu üretir (aşağıdaki blok yaklaşık üç kez tekrarlanır):

addsd       xmm0,mmword ptr [eax+28h]
movsd       mmword ptr [eax+28h],xmm0
movsd       xmm0,mmword ptr [ecx+20h]
addsd       xmm0,mmword ptr [eax+30h]
movsd       mmword ptr [eax+30h],xmm0
movsd       xmm0,mmword ptr [ecx+28h]
addsd       xmm0,mmword ptr [eax+38h]
movsd       mmword ptr [eax+38h],xmm0
movsd       xmm0,mmword ptr [ecx+30h]
addsd       xmm0,mmword ptr [eax+40h]
movsd       mmword ptr [eax+40h],xmm0

Davranış ciddi şekilde dizilerin (n) ve CPU önbelleğinin boyutlarına bağlı olduğundan, sorunun hiçbir önemi yoktur. Bu yüzden daha fazla ilgi varsa, soruyu yeniden ifade ederim:

Aşağıdaki grafikte beş bölge tarafından gösterildiği gibi farklı önbellek davranışlarına yol açan ayrıntılara ilişkin sağlam bir fikir verebilir misiniz?

Bu CPU'lar için benzer bir grafik sağlayarak CPU / önbellek mimarileri arasındaki farklara dikkat çekmek de ilginç olabilir.

PPS: İşte tam kod. Makro tanımlanmadan devre dışı bırakılabilen daha yüksek çözünürlük zamanlaması için TBB kullanır :Tick_CountTBB_TIMING

#include <iostream>
#include <iomanip>
#include <cmath>
#include <string>

//#define TBB_TIMING

#ifdef TBB_TIMING   
#include <tbb/tick_count.h>
using tbb::tick_count;
#else
#include <time.h>
#endif

using namespace std;

//#define preallocate_memory new_cont

enum { new_cont, new_sep };

double *a1, *b1, *c1, *d1;


void allo(int cont, int n)
{
    switch(cont) {
      case new_cont:
        a1 = new double[n*4];
        b1 = a1 + n;
        c1 = b1 + n;
        d1 = c1 + n;
        break;
      case new_sep:
        a1 = new double[n];
        b1 = new double[n];
        c1 = new double[n];
        d1 = new double[n];
        break;
    }

    for (int i = 0; i < n; i++) {
        a1[i] = 1.0;
        d1[i] = 1.0;
        c1[i] = 1.0;
        b1[i] = 1.0;
    }
}

void ff(int cont)
{
    switch(cont){
      case new_sep:
        delete[] b1;
        delete[] c1;
        delete[] d1;
      case new_cont:
        delete[] a1;
    }
}

double plain(int n, int m, int cont, int loops)
{
#ifndef preallocate_memory
    allo(cont,n);
#endif

#ifdef TBB_TIMING   
    tick_count t0 = tick_count::now();
#else
    clock_t start = clock();
#endif

    if (loops == 1) {
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++){
                a1[j] += b1[j];
                c1[j] += d1[j];
            }
        }
    } else {
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                a1[j] += b1[j];
            }
            for (int j = 0; j < n; j++) {
                c1[j] += d1[j];
            }
        }
    }
    double ret;

#ifdef TBB_TIMING   
    tick_count t1 = tick_count::now();
    ret = 2.0*double(n)*double(m)/(t1-t0).seconds();
#else
    clock_t end = clock();
    ret = 2.0*double(n)*double(m)/(double)(end - start) *double(CLOCKS_PER_SEC);
#endif

#ifndef preallocate_memory
    ff(cont);
#endif

    return ret;
}


void main()
{   
    freopen("C:\\test.csv", "w", stdout);

    char *s = " ";

    string na[2] ={"new_cont", "new_sep"};

    cout << "n";

    for (int j = 0; j < 2; j++)
        for (int i = 1; i <= 2; i++)
#ifdef preallocate_memory
            cout << s << i << "_loops_" << na[preallocate_memory];
#else
            cout << s << i << "_loops_" << na[j];
#endif

    cout << endl;

    long long nmax = 1000000;

#ifdef preallocate_memory
    allo(preallocate_memory, nmax);
#endif

    for (long long n = 1L; n < nmax; n = max(n+1, long long(n*1.2)))
    {
        const long long m = 10000000/n;
        cout << n;

        for (int j = 0; j < 2; j++)
            for (int i = 1; i <= 2; i++)
                cout << s << plain(n, m, j, i);
        cout << endl;
    }
}

(Farklı değerleri için FLOP / sn gösterir n.)

resim açıklamasını buraya girin


4
Her eriştiğinizde fiziksel bellekte arama yaparken yavaşlayan ve aynı membrana ikincil erişim durumunda önbellek gibi bir şey olan işletim sistemi olabilir.
AlexTheo

7
Optimizasyonlarla mı derliyorsunuz? O2 için bir sürü asm kodu gibi görünüyor ...
Luchian Grigore

1
Bir süre önce benzer bir soru gibi görünen bir soru sordum . O ya da cevapların ilgi çekici bilgisi olabilir.
Mark Wilkins

61
Sadece seçici olmak için, bu iki kod parçacığı potansiyel olarak çakışan işaretçiler nedeniyle eşdeğer değildir. C99 bu restrictgibi durumlar için anahtar kelimeye sahiptir . MSVC'de benzer bir şey olup olmadığını bilmiyorum. Tabii ki, bu sorun olsaydı, SSE kodu doğru olmazdı.
user510306

8
Bunun bellek takma adıyla bir ilgisi olabilir. Bir döngü ile d1[j]aynı hizaya gelebilir a1[j], bu nedenle derleyici bazı bellek optimizasyonları yapmaktan geri çekilebilir. Yazıları iki döngüde belleğe ayırırsanız bu gerçekleşmez.
rturrado

Yanıtlar:


1690

Bunun daha ayrıntılı analizi üzerine, bunun (en azından kısmen) dört noktanın veri hizalamasından kaynaklandığına inanıyorum. Bu, bazı düzeyde önbellek bankası / yol çakışmalarına neden olacaktır.

Dizilerinizi nasıl ayırdığınızı doğru tahmin ettiysem, büyük olasılıkla sayfa satırına hizalanmış olurlar .

Bu, her döngüdeki tüm erişiminizin aynı önbellek yoluna düşeceği anlamına gelir. Ancak Intel işlemciler bir süredir 8 yönlü L1 önbellek ilişkisine sahipti. Ancak gerçekte, performans tamamen aynı değildir. 4 yönlü erişim, 2 yönlü demekten daha yavaştır.

EDIT: Aslında tüm dizileri ayrı ayrı tahsis gibi görünüyor. Genellikle böyle büyük tahsisler istendiğinde, ayırıcı işletim sisteminden yeni sayfalar isteyecektir. Bu nedenle, büyük ayırmaların sayfa sınırından aynı ofsette görünme olasılığı yüksektir.

İşte test kodu:

int main(){
    const int n = 100000;

#ifdef ALLOCATE_SEPERATE
    double *a1 = (double*)malloc(n * sizeof(double));
    double *b1 = (double*)malloc(n * sizeof(double));
    double *c1 = (double*)malloc(n * sizeof(double));
    double *d1 = (double*)malloc(n * sizeof(double));
#else
    double *a1 = (double*)malloc(n * sizeof(double) * 4);
    double *b1 = a1 + n;
    double *c1 = b1 + n;
    double *d1 = c1 + n;
#endif

    //  Zero the data to prevent any chance of denormals.
    memset(a1,0,n * sizeof(double));
    memset(b1,0,n * sizeof(double));
    memset(c1,0,n * sizeof(double));
    memset(d1,0,n * sizeof(double));

    //  Print the addresses
    cout << a1 << endl;
    cout << b1 << endl;
    cout << c1 << endl;
    cout << d1 << endl;

    clock_t start = clock();

    int c = 0;
    while (c++ < 10000){

#if ONE_LOOP
        for(int j=0;j<n;j++){
            a1[j] += b1[j];
            c1[j] += d1[j];
        }
#else
        for(int j=0;j<n;j++){
            a1[j] += b1[j];
        }
        for(int j=0;j<n;j++){
            c1[j] += d1[j];
        }
#endif

    }

    clock_t end = clock();
    cout << "seconds = " << (double)(end - start) / CLOCKS_PER_SEC << endl;

    system("pause");
    return 0;
}

Deney Sonuçları:

EDIT: Gerçek bir Core 2 mimarisi makinesinde sonuçlar:

2 x Intel Xeon X5482 Harpertown @ 3.2 GHz:

#define ALLOCATE_SEPERATE
#define ONE_LOOP
00600020
006D0020
007A0020
00870020
seconds = 6.206

#define ALLOCATE_SEPERATE
//#define ONE_LOOP
005E0020
006B0020
00780020
00850020
seconds = 2.116

//#define ALLOCATE_SEPERATE
#define ONE_LOOP
00570020
00633520
006F6A20
007B9F20
seconds = 1.894

//#define ALLOCATE_SEPERATE
//#define ONE_LOOP
008C0020
00983520
00A46A20
00B09F20
seconds = 1.993

Gözlemler:

  • Bir döngü ile 6.206 saniye ve iki döngü ile 2.116 saniye . Bu OP'nin sonuçlarını aynen üretir.

  • İlk iki testte, diziler ayrı olarak ayrılır. Hepsinin sayfaya göre aynı hizalamaya sahip olduğunu fark edeceksiniz.

  • İkinci iki testte, diziler bu hizalamayı kırmak için bir araya getirilir. Burada her iki devrenin de daha hızlı olduğunu fark edeceksiniz. Ayrıca, ikinci (çift) döngü artık normalde beklediğiniz gibi daha yavaş olan döngüdür.

@Stephen Cannon'un yorumlarda belirttiği gibi, bu hizalamanın yükleme / depolama birimlerinde veya önbellekte yanlış örtüşmeye neden olma olasılığı çok yüksektir . Bunu araştırdım ve Intel'in kısmi adres takma tezgahları için bir donanım sayacına sahip olduğunu buldum :

http://software.intel.com/sites/products/documentation/doclib/stdxe/2013/~amplifierxe/pmw_dp/events/partial_address_alias.html


5 Bölgeler - Açıklamalar

Bölge 1:

Bu kolay. Veri kümesi o kadar küçüktür ki performans havai döngü ve dallanma gibi üstünlüklere sahiptir.

Bölge 2:

Burada, veri boyutları arttıkça, göreli ek yük miktarı azalır ve performans "doyurulur". Burada iki döngü daha yavaştır, çünkü iki kat daha fazla döngü ve dallanma yükü vardır.

Burada neler olup bittiğinden tam olarak emin değilim ... Agner Fog'un önbellek bankası çakışmalarından bahsettiği için uyum hala etkili olabilir . (Bu bağlantı Sandy Bridge ile ilgilidir, ancak fikir hala Core 2 için geçerli olmalıdır.)

Bölge 3:

Bu noktada, veriler artık L1 önbelleğine sığmaz. Performans L1 <-> L2 önbellek bant genişliği ile sınırlıdır.

Bölge 4:

Tek döngüdeki performans düşüşü gözlemlediğimiz şeydir. Ve belirtildiği gibi, bu, işlemci yükü / depolama birimlerinde (büyük olasılıkla) yanlış örtüşme duraklarına neden olan hizalamadan kaynaklanmaktadır.

Bununla birlikte, yanlış takma adın oluşması için veri kümeleri arasında yeterince büyük bir adım olmalıdır. Bu yüzden bunu 3. bölgede görmüyorsunuz.

Bölge 5:

Bu noktada, önbelleğe hiçbir şey sığmaz. Yani bellek bant genişliğine bağlısınız.


2 x Intel X5482 Harpertown @ 3,2 GHz 2,8 GHz'de Intel Core i7 870 Intel Core i7 2600K @ 4,4 GHz


162
+1: Bence cevap bu. Diğer tüm yanıtların söylediklerinin aksine, tek döngü varyantının doğası gereği daha fazla önbellek özlemine sahip olmasıyla ilgili değildir, önbellek özlemlerine neden olan dizilerin belirli bir hizalaması ile ilgilidir.
Oliver Charlesworth

30
Bu; Bir yanlış örtüşme durak en olası açıklama.
Stephen Canon

7
@VictorT. OP'nin bağlı olduğu kodu kullandım. Excel'de açıp ondan bir grafik oluşturabileceğim bir .css dosyası oluşturur.
Gizemli

5
@Nawaz Bir sayfa genellikle 4KB'dir. Yazdırdığım onaltılı adreslere bakarsanız, ayrı olarak ayrılmış testlerin tümü aynı modulo 4096'ya sahiptir. (4KB sınırının başlangıcından itibaren 32 bayttır) Belki de GCC bu davranışa sahip değildir. Bu, neden farklılıkları görmediğinizi açıklayabilir.
Mistik


224

Tamam, doğru cevap kesinlikle CPU önbelleği ile bir şeyler yapmak zorunda. Ancak önbellek argümanını kullanmak, özellikle veri olmadan oldukça zor olabilir.

Çok fazla tartışmaya yol açan birçok cevap var, ama bununla yüzleşelim: Önbellek sorunları çok karmaşık olabilir ve tek boyutlu olmayabilir. Büyük ölçüde verilerin boyutuna bağlılar, bu yüzden sorum haksızdı: Önbellek grafiğinde çok ilginç bir noktada olduğu ortaya çıktı.

@ Mysticial'ın cevabı, muhtemelen gerçeklere güvenen tek kişi olduğu için (benim de dahil) birçok insanı ikna etti, ancak gerçeğin sadece bir "veri noktası" idi.

Bu yüzden testini (sürekli ve ayrı bir ayırma kullanarak) ve @James 'Answer'in tavsiyesini birleştirdim.

Aşağıdaki grafikler, soruya verilen cevapların çoğunun ve özellikle yorumların çoğunun, kullanılan senaryoya ve parametrelere bağlı olarak tamamen yanlış veya doğru olarak değerlendirilebileceğini göstermektedir.

İlk sorumun n = 100.000 olduğunu unutmayın . Bu nokta (kazayla) özel davranış gösterir:

  1. Bir ve iki loop'lu versiyon arasında en büyük tutarsızlığa sahiptir (neredeyse üç faktör)

  2. Tek loop'un (sürekli tahsis ile) iki looplu versiyonu geçtiği tek noktadır. (Bu Mysticial'ın cevabını mümkün kıldı.)

Başlatılan verileri kullanan sonuç:

Resim açıklamasını buraya girin

Başlatılmamış verileri kullanan sonuç (Mysticial test ettiği şey budur):

Resim açıklamasını buraya girin

Ve bu açıklanması zor bir şeydir: Bir kez tahsis edilen ve farklı vektör boyutlarındaki aşağıdaki her test durumu için yeniden kullanılan başlatılmış veriler:

Resim açıklamasını buraya girin

öneri

Önbellekle ilgili tüm veri boyutları aralığı için MFLOPS bilgileri sağlamak için Yığın Taşması ile ilgili her düşük düzey performansla ilgili soru gereklidir! Yanıtları düşünmek ve özellikle bu bilgi olmadan başkalarıyla tartışmak herkes için zaman kaybıdır.


18
+1 Güzel analiz. İlk başta verileri başlangıçta bırakmak istemedim. Allocator onları zaten sıfırladı. Başlatılmış veriler önemlidir. Cevabımı gerçek bir Core 2 mimari makinesindeki sonuçlarla düzenledim ve gözlemlediğinize çok daha yakınlar. Başka bir şey, bir dizi boyutu test ettim nve n = 80000, n = 100000, n = 200000vb. İçin aynı performans boşluğunu gösteriyor ...
Mysticial

2
@Mysticial Bence işletim sistemi olası süreçler arası casusluk önlemek için bir işleme her yeni sayfa verirken sayfa sıfırlama uygular.
v.oddou

1
@ v.oddou: Davranış işletim sistemine de bağlıdır; IIRC, Windows, sıfırlanmış serbest sayfaların arka planına bir iş parçacığı içerir ve zaten sıfırlanmış sayfalardan bir istek karşılanamazsa, VirtualAllocçağrı, isteği karşılamak için yeterince sıfırlanana kadar engeller. Buna karşılık, Linux sadece sıfır sayfayı yazarken kopyalanacak kadar eşler ve yazarken, yeni verileri yazmadan önce yeni sıfırları yeni bir sayfaya kopyalar. Her iki durumda da, kullanıcı modu işleminin bakış açısından, sayfalar sıfırlanır, ancak başlatılmamış belleğin ilk kullanımı Linux'ta genellikle Windows'dan daha pahalı olur.
ShadowRanger

81

İkinci döngü çok daha az önbellek etkinliği içerir, bu nedenle işlemcinin bellek taleplerini karşılaması daha kolaydır.


1
İkinci varyantın daha az önbellek kaybına uğradığını mı söylüyorsunuz? Neden?
Oliver Charlesworth

2
@Oli: • birinci varyant olarak, işlemci, bir zaman-de erişim dört bellek hatları gerekiyor a[i], b[i], c[i]ve d[i]ikinci varyantta, iki ihtiyacı vardır. Bu, eklerken bu hatların doldurulmasını çok daha uygun hale getirir.
Köpek yavrusu

4
Ancak diziler önbellekte çarpışmadığı sürece, her bir varyant ana bellekten / belleğe aynı sayıda okuma ve yazma gerektirir. Sonuç olarak, bu iki dizinin sürekli çarpıştığı sonucuna varıyorum.
Oliver Charlesworth

3
Takip etmiyorum. Talimat başına (yani her örnek için x += y), iki okuma ve bir yazma vardır. Bu, her iki değişken için de geçerlidir. Önbellek <-> CPU bant genişliği gereksinimi aynıdır. Çakışma olmadığı sürece, önbellek <-> RAM bant genişliği gereksinimi de aynıdır ..
Oliver Charlesworth

2
Stackoverflow.com/a/1742231/102916 içinde belirtildiği gibi , Pentium M'in donanım ön alımı 12 farklı ileri akışı izleyebilir (ve daha sonra donanımın en azından mümkün olduğu kadar beklenebilir). Döngü 2 hala sadece dört akışı okuyor, bu yüzden bu sınırın içinde.
Brooks Moses

50

Bir ndizide yalnızca dizilerinizin ikisini aynı anda bellekte tutabilmeniz için doğru değere sahip bir makine üzerinde çalıştığınızı , ancak disk önbelleğe alma yoluyla kullanılabilir toplam belleğin dördünü de tutmak için yeterli olduğunu düşünün.

Basit bir LIFO önbellek politikası varsayarsak, bu kod:

for(int j=0;j<n;j++){
    a[j] += b[j];
}
for(int j=0;j<n;j++){
    c[j] += d[j];
}

ister ilk nedeni ave bdaha sonra RAM içine yüklenmiş ve RAM tamamen çalışılabilir. İkinci döngü başladığında cve ddaha sonra diskten RAM'e yüklenir ve çalıştırılır.

diğer döngü

for(int j=0;j<n;j++){
    a[j] += b[j];
    c[j] += d[j];
}

döngü etrafında her seferinde iki dizi ve diğer ikisinde sayfa oluşturur . Bu çok daha yavaş olurdu .

Muhtemelen testlerinizde disk önbellekleme görmüyorsunuz, ancak muhtemelen başka bir önbellekleme biçiminin yan etkilerini görüyorsunuz.


Burada biraz karışıklık / yanlış anlama var, bu yüzden bir örnek kullanarak biraz ayrıntıya girmeye çalışacağım.

Söyle n = 2ve baytlarla çalışıyoruz. Benim senaryomda bu nedenle sadece 4 bayt RAM var ve belleğimizin geri kalanı önemli ölçüde daha yavaş (100 kat daha uzun erişim).

Oldukça aptal bir önbellek politikası varsayarsak Bayt değilse, , oraya koy ve biz de şu baytı da alırız, şöyle bir senaryo alırsınız:

  • İle

    for(int j=0;j<n;j++){
     a[j] += b[j];
    }
    for(int j=0;j<n;j++){
     c[j] += d[j];
    }
  • önbellek a[0]ve a[1]sonra b[0]ve b[1]vea[0] = a[0] + b[0] önbellekte - şimdi önbellekte dört bayt vardır a[0], a[1]ve b[0], b[1]. Maliyet = 100 + 100.

  • a[1] = a[1] + b[1]önbellekte ayarla . Maliyet = 1 + 1.
  • İçin tekrarla c ve d.
  • Toplam maliyet = (100 + 100 + 1 + 1) * 2 = 404

  • İle

    for(int j=0;j<n;j++){
     a[j] += b[j];
     c[j] += d[j];
    }
  • önbellek a[0]ve a[1]sonra b[0]ve b[1]ve a[0] = a[0] + b[0]önbellekte ayarla - şimdi önbellekte dört bayt var,a[0], a[1]ve b[0], b[1]. Maliyet = 100 + 100.

  • Çıkartma a[0], a[1], b[0], b[1]önbelleği ve önbellekten c[0]ve c[1]ardından d[0]ved[1]c[0] = c[0] + d[0] önbelleğe ayarlayın . Maliyet = 100 + 100.
  • Nereye gittiğimi görmeye başladığından şüpheleniyorum.
  • Toplam maliyet = (100 + 100 + 100 + 100) * 2 = 800

Bu klasik bir önbellek thrash senaryosudur.


12
Bu yanlış. Bir dizinin belirli bir öğesine yapılan başvuru, dizinin tamamının diskten (veya önbelleğe alınmamış bellekten) sayfalanmasına neden olmaz; yalnızca ilgili sayfa veya önbellek satırı çağrılır.
Brooks Moses

1
@Brooks Moses - Eğer burada olduğu gibi tüm dizi üzerinden yürümek, o zaman olacak.
OldCurmudgeon

1
Evet, ama tüm operasyon boyunca olan şey bu, döngü etrafında her seferinde olan şey değil. İkinci formun "döngü etrafında her seferinde iki diziyi ve diğer ikisinde sayfa oluşturacağını" iddia ettiniz ve buna itiraz ediyorum. Genel dizilerin boyutuna bakılmaksızın, bu döngünün ortasında RAM'iniz dört dizinin her birinden bir sayfa tutacaktır ve döngü bittikten sonraya kadar hiçbir şey sayfalanmayacaktır.
Brooks Moses

N'nin sadece doğru değer olduğu durumda , dizilerinizin ikisini aynı anda bellekte tutabilmeniz ve daha sonra bir dizideki dört dizinin tüm elemanlarına erişmeniz kesinlikle çökecektir.
OldCurmudgeon

1
Neden bir bütünlüğü içinde bu döngü 2 sayfaları kalıyorsun a1ve b1oldukça bunlardan her birinin sadece ilk sayfaya göre, ilk atama için? (5 baytlık sayfalar mı varsayıyorsunuz, bu yüzden bir sayfa RAM'inizin yarısı mı? Bu sadece ölçeklendirme değil, bu gerçek bir işlemciden tamamen farklı.)
Brooks Moses

35

Farklı bir kod nedeniyle değil, önbellek nedeniyle: RAM, CPU kayıtlarından daha yavaştır ve bir değişken her değiştiğinde RAM'i yazmaktan kaçınmak için CPU'nun içinde bir önbellek vardır. Ancak önbellek RAM olduğu için büyük değildir, bu nedenle RAM'in sadece bir kısmını eşler.

Birinci kod, uzak bellek adreslerini her döngüde değiştirerek değiştirir, böylece önbelleğin sürekli olarak geçersiz kılınmasını gerektirir.

İkinci kod değişmez: sadece bitişik adreslere iki kez akar. Bu, tüm işi önbellekte tamamlar ve yalnızca ikinci döngü başladıktan sonra geçersiz kılar.


Bu neden önbelleğin sürekli geçersiz kılınmasına neden olur?
Oliver Charlesworth

1
@OliCharlesworth: Önbelleği bitişik bir dizi bellek adresinin basılı kopyası olarak düşünün. Bir parçası olmayan bir adrese erişiyormuş gibi yaparsanız, önbelleği yeniden yüklemeniz gerekir. Ve önbellekteki bir şey değiştirilmişse, RAM'e geri yazılmalıdır, yoksa kaybolacaktır. Örnek kodda, 100.000 tamsayıdan (400kBayt) oluşan 4 vektör büyük olasılıkla L1 önbelleğinin (128 veya 256K) kapasitesinden daha fazladır.
Emilio Garavaglia

5
Bu senaryoda önbellek boyutunun bir etkisi yoktur. Her dizi öğesi yalnızca bir kez kullanılır ve bundan sonra tahliye edilip edilmemesi önemli değildir. Önbellek boyutu yalnızca geçici konumunuz varsa önemlidir (yani gelecekte aynı öğeleri tekrar kullanacaksınız).
Oliver Charlesworth

2
@OliCharlesworth: Bir önbelleğe yeni bir değer yüklemem gerekiyorsa ve içinde değiştirilmiş bir değer zaten varsa, önce yazmam gerekir ve bu yazma işleminin gerçekleşmesini bekler.
Emilio Garavaglia

2
Ancak OP kodunun her iki varyantında, her değer bir kez tam olarak değiştirilir. Bunu her varyantta aynı sayıda geri yazma yaparsınız.
Oliver Charlesworth

22

Burada tartışılan sonuçları çoğaltamıyorum.

Kötü kıyaslama kodunun suçlanıp suçlanmayacağını veya ne olduğunu bilmiyorum, ancak iki yöntem aşağıdaki kodu kullanarak makinemde birbirlerinin% 10'u içinde ve bir döngü genellikle ikiden biraz daha hızlı - bekliyoruz.

Dizi boyutları sekiz döngü kullanılarak 2 ^ 16 ile 2 ^ 24 arasında değişiyordu. Kaynak dizileri başlatmaya dikkat ettim, böylece +=atama FPU'ya sormadı çift ​​olarak yorumlanan bellek çöpü eklemesini .

Böyle atama koyarak gibi çeşitli düzenleri, etrafında oynanan b[j], d[j]karşı InitToZero[j]kullanarak da döngüler içinde ve += b[j] = 1ve+= d[j] = 1 , ve ben oldukça tutarlı sonuçlar aldık.

Eğer başlatılıyor, Tahmin edebileceğiniz gibi bve ddöngü içinde kullanarak InitToZero[j]onlar için atamaları önce sırt sırta için yapıldı gibi bir avantaj kombine yaklaşım verdi avec , ancak yine de% 10 dahilinde. Git şekil.

Donanım nesil 3 Core i7 ile Dell XPS 8500 @ 3.4 GHz ve 8 GB belleğe . Sekiz döngü kullanan 2 ^ 16 ila 2 ^ 24 için kümülatif süre sırasıyla 44.987 ve 40.965 idi. Visual C ++ 2010, tamamen optimize edilmiştir.

Not: Döngüleri sıfıra indirecek şekilde değiştirdim ve kombine yöntem marjinal olarak daha hızlıydı. Başımı tırmalamak. Yeni dizi boyutlandırma ve döngü sayılarına dikkat edin.

// MemBufferMystery.cpp : Defines the entry point for the console application.
//
#include "stdafx.h"
#include <iostream>
#include <cmath>
#include <string>
#include <time.h>

#define  dbl    double
#define  MAX_ARRAY_SZ    262145    //16777216    // AKA (2^24)
#define  STEP_SZ           1024    //   65536    // AKA (2^16)

int _tmain(int argc, _TCHAR* argv[]) {
    long i, j, ArraySz = 0,  LoopKnt = 1024;
    time_t start, Cumulative_Combined = 0, Cumulative_Separate = 0;
    dbl *a = NULL, *b = NULL, *c = NULL, *d = NULL, *InitToOnes = NULL;

    a = (dbl *)calloc( MAX_ARRAY_SZ, sizeof(dbl));
    b = (dbl *)calloc( MAX_ARRAY_SZ, sizeof(dbl));
    c = (dbl *)calloc( MAX_ARRAY_SZ, sizeof(dbl));
    d = (dbl *)calloc( MAX_ARRAY_SZ, sizeof(dbl));
    InitToOnes = (dbl *)calloc( MAX_ARRAY_SZ, sizeof(dbl));
    // Initialize array to 1.0 second.
    for(j = 0; j< MAX_ARRAY_SZ; j++) {
        InitToOnes[j] = 1.0;
    }

    // Increase size of arrays and time
    for(ArraySz = STEP_SZ; ArraySz<MAX_ARRAY_SZ; ArraySz += STEP_SZ) {
        a = (dbl *)realloc(a, ArraySz * sizeof(dbl));
        b = (dbl *)realloc(b, ArraySz * sizeof(dbl));
        c = (dbl *)realloc(c, ArraySz * sizeof(dbl));
        d = (dbl *)realloc(d, ArraySz * sizeof(dbl));
        // Outside the timing loop, initialize
        // b and d arrays to 1.0 sec for consistent += performance.
        memcpy((void *)b, (void *)InitToOnes, ArraySz * sizeof(dbl));
        memcpy((void *)d, (void *)InitToOnes, ArraySz * sizeof(dbl));

        start = clock();
        for(i = LoopKnt; i; i--) {
            for(j = ArraySz; j; j--) {
                a[j] += b[j];
                c[j] += d[j];
            }
        }
        Cumulative_Combined += (clock()-start);
        printf("\n %6i miliseconds for combined array sizes %i and %i loops",
                (int)(clock()-start), ArraySz, LoopKnt);
        start = clock();
        for(i = LoopKnt; i; i--) {
            for(j = ArraySz; j; j--) {
                a[j] += b[j];
            }
            for(j = ArraySz; j; j--) {
                c[j] += d[j];
            }
        }
        Cumulative_Separate += (clock()-start);
        printf("\n %6i miliseconds for separate array sizes %i and %i loops \n",
                (int)(clock()-start), ArraySz, LoopKnt);
    }
    printf("\n Cumulative combined array processing took %10.3f seconds",
            (dbl)(Cumulative_Combined/(dbl)CLOCKS_PER_SEC));
    printf("\n Cumulative seperate array processing took %10.3f seconds",
        (dbl)(Cumulative_Separate/(dbl)CLOCKS_PER_SEC));
    getchar();

    free(a); free(b); free(c); free(d); free(InitToOnes);
    return 0;
}

MFLOPS'un neden ilgili bir metrik olduğuna karar verildiğinden emin değilim. Fikir bellek erişimine odaklanmaktı, ama kayan nokta hesaplama süresini en aza indirmeye çalıştım. İçeri girdim +=, ama neden olduğundan emin değilim.

Hesaplama yapılmayan düz bir atama, bellek erişim süresinin daha açık bir testi olacaktır ve döngü sayısından bağımsız olarak eşit bir test oluşturacaktır. Belki sohbette bir şey kaçırdım, ama iki kez düşünmeye değer. Artı, ödevin dışında bırakılırsa, toplam süre her biri 31 saniyede neredeyse aynıdır.


1
Burada belirttiğiniz yanlış hizalama cezası, yanlış hizalanmış tek bir yükün / deponun (hizalanmamış SSE yükü / depoları dahil) olmasıdır. Ancak performans farklı dizilerin göreli hizalamalarına duyarlı olduğu için bu durum böyle değildir. Talimat düzeyinde yanlış hizalama yoktur. Her bir yük / depo uygun şekilde hizalanmıştır.
Gizemli

18

Çünkü CPU çok fazla önbellek kaybına sahip değildir (dizi verilerinin RAM çiplerinden gelmesini beklemek zorundadır). CPU'nuzun seviye 1 önbellek (L1) ve sonra seviye 2 önbellek (L2) boyutlarını aşacak ve kodunuz için harcanan zamanı çizecek şekilde dizilerin boyutunu sürekli olarak ayarlamanız ilginç olacaktır . dizilerin boyutlarına karşı yürütmek. Grafik beklediğiniz gibi düz bir çizgi olmamalıdır.


2
Önbellek boyutu ile dizi boyutu arasında herhangi bir etkileşim olduğuna inanmıyorum. Her dizi öğesi yalnızca bir kez kullanılır ve daha sonra güvenli bir şekilde tahliye edilebilir. Öte yandan , dört dizinin çakışmasına neden oluyorsa, önbellek satırı boyutu ile dizi boyutu arasında bir etkileşim olabilir .
Oliver Charlesworth

15

İlk döngü dönüşümlü olarak her değişkende yazılır. İkinci ve üçüncü olanlar sadece eleman boyutunda küçük sıçramalar yapar.

20 cm'lik bir kalem ve kağıtla 20 haçlık iki paralel çizgi yazmayı deneyin. Bir kez ve sonra diğer satırı bitirmeyi deneyin ve dönüşümlü olarak her satıra bir çarpı işareti yazarak başka bir zaman deneyin.


CPU talimatları gibi şeyleri düşünürken gerçek dünyadaki faaliyetlerin analojileri tehlikeyle doludur. Gösterdiğiniz şey , dönen bir diskte depolanan verileri okuma / yazma hakkında konuşuyorsak geçerli olacak zamanı etkili bir şekilde aramaktır , ancak CPU önbelleğinde (veya RAM'de veya SSD'de) arama süresi yoktur. Belleğin ayrık bölgelerine erişim, bitişik erişimlere karşı cezalandırılmaz.
FeRD

7

Orijinal Soru

Bir döngü neden iki döngüden daha yavaş?


Sonuç:

Dava 1 verimsiz olan klasik bir enterpolasyon problemidir. Bunun, birçok makine mimarisinin ve geliştiricisinin, paralel işlemenin yanı sıra çok iş parçacıklı uygulamalar yapma yeteneğine sahip çok çekirdekli sistemler oluşturup tasarlamasının önde gelen nedenlerinden biri olduğunu düşünüyorum.

Donanım, İşletim Sistemi ve Derleyici (ler) in RAM, Önbellek, Sayfa Dosyaları vb. İle çalışmayı içeren yığın tahsisleri yapmak için birlikte nasıl çalıştığını içermeden bu tür bir yaklaşımdan bakmak; bu algoritmaların temelini oluşturan matematik bize bu ikisinden hangisinin daha iyi bir çözüm olduğunu gösterir.

Biz bir benzetme kullanabilirsiniz Bossbir varlık Summationbir temsil edeceğini For Loopişçiler arasında seyahat gerektiğini A& B.

Biz kolayca görebiliriz Durum 2 hızlı birden değil biraz sanki az yarısı olan Durumunda 1 nedeniyle seyahat ve işçiler arasında geçen zamandır ihtiyaç duyulan mesafe farkı. Bu matematik, neredeyse BenchMark Times'ın yanı sıra Montaj Talimatları'ndaki farklılıkların sayısı ile neredeyse sanal ve mükemmel bir şekilde sıralanır.


Şimdi tüm bunların nasıl çalıştığını aşağıda açıklamaya başlayacağım.


Sorunu Değerlendirme

OP'nin kodu:

const int n=100000;

for(int j=0;j<n;j++){
    a1[j] += b1[j];
    c1[j] += d1[j];
}

Ve

for(int j=0;j<n;j++){
    a1[j] += b1[j];
}
for(int j=0;j<n;j++){
    c1[j] += d1[j];
}

Düşünme

OP'nin döngülerin 2 varyantı hakkındaki orijinal sorusu ve önbelleklerin davranışına yönelik değiştirilmiş sorusu ve diğer birçok mükemmel cevap ve faydalı yorum göz önüne alındığında; Bu durum ve sorun hakkında farklı bir yaklaşım benimseyerek burada farklı bir şeyler yapmak istiyorum.


Yaklaşım

İki döngüyü ve önbellek ve sayfa dosyalama hakkındaki tüm tartışmaları göz önünde bulundurarak, buna farklı bir perspektiften bakmak için başka bir yaklaşım kullanmak istiyorum. Önbellek ve sayfa dosyalarını veya bellek ayırma işlemlerini içermeyen bir şey, aslında, bu yaklaşım gerçek donanım veya yazılımla bile ilgili değildir.


Bakış açısı

Bir süre koda baktıktan sonra sorunun ne olduğu ve neyin üretildiği oldukça belirgin hale geldi. Bunu algoritmik bir soruna ayıralım ve matematiksel gösterimleri kullanma perspektifinden bakalım, daha sonra matematik problemlerine ve algoritmalara bir benzetme uygulayalım.


Ne Biliyoruz

Bu döngünün 100.000 kez çalışacağını biliyoruz. Biz de biliyoruz a1, b1, c1ved1 64-bit mimarisine göstericisidir. 32 bitlik bir makinedeki C ++ içinde, tüm işaretçiler 4 bayt ve 64 bitlik bir makinede, işaretçiler sabit uzunlukta olduğundan 8 bayt boyutundadır.

Her iki durumda da ayırmamız gereken 32 bayt olduğunu biliyoruz. Tek fark, her yinelemede 32 bayt veya 2 set 2-8bayt tahsis etmemizdir; burada 2. durum, her iki bağımsız döngü için her yineleme için 16 bayt tahsis ederiz.

Her iki döngü de toplam ayırmada 32 bayta eşittir. Bu bilgi ile şimdi devam edelim ve bu kavramların genel matematiğini, algoritmalarını ve benzetmesini gösterelim.

Her iki durumda da aynı grup veya işlem grubunun kaç kez yapılması gerektiğini biliyoruz. Her iki durumda da tahsis edilmesi gereken bellek miktarını biliyoruz. Her iki durum arasındaki tahsisatların toplam iş yükünün yaklaşık olarak aynı olacağını değerlendirebiliriz.


Bilmediklerimiz

Bir sayaç ayarlayıp bir kıyaslama testi yapmazsak, her dava için ne kadar süreceğini bilmiyoruz. Ancak, kriterler orijinal sorudan ve bazı cevap ve yorumlardan zaten dahil edildi; ve ikisi arasında önemli bir fark olduğunu görebiliriz ve bu sorun için bu teklifin tüm sebebi budur.


Araştıralım

Birçok kişinin bunu zaten yığın tahsislerine, kıyaslama testlerine, RAM, Önbellek ve Sayfa Dosyalarına bakarak zaten yapmış olduğu açıktır. Belirli veri noktalarına ve spesifik yineleme indekslerine bakıldığında da dahil edildi ve bu özel sorunla ilgili çeşitli görüşmeler birçok insanın ilgili diğer şeyleri sorgulamaya başladı. Matematiksel algoritmalar kullanarak ve buna benzetme yaparak bu soruna nasıl bakmaya başlarız? Birkaç iddia yaparak başlıyoruz! Sonra algoritmamızı oradan oluştururuz.


İddialarımız:

  • Döngümüzün ve yinelemelerinin, döngülerde olduğu gibi 0 ile başlamak yerine 1'de başlayan ve 100000'de biten bir Summation olmasına izin vereceğiz, çünkü sadece ilgilendiğimiz için bellek adreslemesinin 0 indeksleme şeması hakkında endişelenmemize gerek yok. algoritmanın kendisi.
  • Her iki durumda da, üzerinde çalışacağımız 4 fonksiyon ve her fonksiyon çağrısında 2 işlem yapılmış 2 fonksiyon çağrımız vardır. Biz Aşağıdaki gibi fonksiyonlara işlevleri ve çağrılar gibi bu kadar ayarlayacaktır: F1(), F2(), f(a), f(b), f(c)ve f(d).

Algoritmalar:

1. Durum: - Sadece bir toplam değil, iki bağımsız fonksiyon çağrısı.

Sum n=1 : [1,100000] = F1(), F2();
                       F1() = { f(a) = f(a) + f(b); }
                       F2() = { f(c) = f(c) + f(d); }

2. Durum: - İki özet ama her birinin kendi işlev çağrısı vardır.

Sum1 n=1 : [1,100000] = F1();
                        F1() = { f(a) = f(a) + f(b); }

Sum2 n=1 : [1,100000] = F1();
                        F1() = { f(c) = f(c) + f(d); }

Fark ise F2()sadece var olan Sumdan Case1nerede F1()bulunur Sumgelen Case1ve her ikisi de Sum1ve Sum2gelen Case2. Bu, daha sonra ikinci algoritma içinde gerçekleşen bir optimizasyon olduğu sonucuna vardığımızda açıklık kazanacaktır.

İlk durum Sumçağrıları aracılığıyla f(a)kendi kendine eklenecek yinelemeler f(b)sonra f(c)aynı şeyi yapacak ancak f(d)her 100000yineleme için kendine ekleyecek çağrılar . İkinci durumda, elimizdeki Sum1ve Sum2aynı işlevi arka arkaya iki kez çağrıldığını sanki her ikisi de aynı etki yaptığını göstermektedir.

Bu durumda biz tedavi edebilir Sum1ve Sum2sadece düz eski olarak Sumnerede Sumbu durumda böyle görünüyor: Sum n=1 : [1,100000] { f(a) = f(a) + f(b); }şimdi ve biz sadece aynı işlevi olarak düşünebilirsiniz bir optimizasyon gibi bu görünüyor.


Analoji ile Özet

İkinci durumda gördüklerimizle birlikte, her iki döngü için de aynı imzası olduğu için neredeyse bir optimizasyon varmış gibi görünüyor, ancak bu gerçek sorun değil. Sorun tarafından yapılıyor işi değil f(a), f(b), f(c)vef(d) . Her iki durumda ve ikisi arasındaki karşılaştırma, Toplama'nın seyahat etmesi gereken mesafedeki farktır, bu da size yürütme süresindeki farkı verir.

Düşünün For Loopsolarak Summationsbir varlık olarak yinelemeleri yaptığı Bossiki kişiye emir verdiğini A& Bve işlerini ete olduğunu Cve Dsırasıyla ve onlardan bazı paket almak ve bunu iade etmek. Bu benzetmede, for döngüler veya toplama yinelemeleri ve durum kontrolleri kendilerini gerçekten temsil etmez Boss. Aslında Boss, gerçek matematiksel algoritmalardan doğrudan değil, rutin veya altyordam, yöntem, işlev, çeviri birimi vb .'nin gerçek kavramından Scopeve Code Blockiçinde. İlk algoritmanın, 2. algoritmanın ardışık 2 kapsamı olduğu 1 kapsamı vardır.

Her çağrı fişinde İlk durumda içinde, Bossgider Ave emir verir ve Aalmaya söner B'ssonra paketi Bossgider Cve aynı şeyi ve paketi almak için emir verir Dher tekrarında.

İkinci durumda, tüm paketler alınana kadar paketi almak ve almak için Bossdoğrudan çalışır . Sonra tüm paketleri almak için aynı şeyi yapmak için çalışır .AB'sBossCD's

8 baytlık bir işaretçi ile çalıştığımızdan ve yığın tahsisi ile uğraştığımızdan, aşağıdaki sorunu ele alalım. Diyelim ki Boss100 metre Ave A500 metre C. İnfazların sırası nedeniyle Bossbaşlangıçta ne kadar uzakta olduğu konusunda endişelenmemize gerek yok C. Her iki durumda da, Bossbaşlangıçta geçecek Ailk önce için B. Bu benzetme, bu mesafenin kesin olduğunu söylemez; sadece algoritmaların işleyişini göstermek için yararlı bir test senaryosu.

Birçok durumda, yığın ayırmaları yaparken ve önbellek ve sayfa dosyalarıyla çalışırken, adres konumları arasındaki bu mesafeler çok fazla değişmeyebilir veya veri türlerinin doğasına ve dizi boyutlarına bağlı olarak önemli ölçüde değişebilir.


Test Durumları:

Birinci Durum: İlk iterasyonda,Bosssipariş fişini vermek için başlangıçta 100 feet gitmeliAveAgidip bir şey yapmalı, ancak daha sonraona sipariş fişini vermekBossiçin 500 feet seyahatCetmelidir. Sonra bir sonraki iterasyonda ve diğer her iterasyondaBossikisi arasında 500 feet ileri geri gitmek zorunda.

İkinci Olgu:Boss ilk tekrarında 100 ayak seyahat etmek olanA, ama bundan sonra, o zaten orada ve sadece beklerAtüm slipleri dolana kadar geri kazanmak için. O zamanBossilk iterasyonda 500 feet seyahat etmek zorundaCçünküC500 metre uzaklıktadırA. BuBoss( Summation, For Loop ), çalıştıktan hemen sonra çağrıldığından,tümsipariş fişleriyapılana kadarAorada olduğu gibi bekler. AC's


Seyahat Edilen Mesafeler Arasındaki Fark

const n = 100000
distTraveledOfFirst = (100 + 500) + ((n-1)*(500 + 500); 
// Simplify
distTraveledOfFirst = 600 + (99999*100);
distTraveledOfFirst = 600 + 9999900;
distTraveledOfFirst =  10000500;
// Distance Traveled On First Algorithm = 10,000,500ft

distTraveledOfSecond = 100 + 500 = 600;
// Distance Traveled On Second Algorithm = 600ft;    

Keyfi Değerlerin Karşılaştırılması

600'ün 10 milyondan çok daha az olduğunu kolayca görebiliriz. Şimdi, bu kesin değil, çünkü RAM'in hangi adresi ile her bir yinelemedeki her bir çağrının hangi Önbellek veya Sayfa Dosyasından diğer görünmeyen değişkenlerden kaynaklanacağı arasındaki gerçek farkı bilmiyoruz. Bu sadece farkında olmak ve en kötü senaryodan bakmak için durumun bir değerlendirmesidir.

Bu rakamlardan neredeyse Algoritma Bir Algoritma 99%İki'den daha yavaş olmalı gibi görünecektir ; Ancak bu sadece Boss'sbir parça veya algoritmaların sorumluluk ve gerçek işçiler dikkate almaz A, B, C, ve Dve ne her ve Loop her tekrarında yapmak zorundayız. Böylece patronun işi yapılan toplam işin sadece% 15 - 40'ını oluşturur. İşçiler aracılığıyla yapılan işin büyük kısmı, hız oranı farklarının oranını yaklaşık% 50-70'e tutma yönünde biraz daha büyük bir etkiye sahiptir.


Gözlem: - İki algoritma arasındaki farklar

Bu durumda, yapılan işin sürecinin yapısıdır. Durum 2'nin benzer bir işlev beyanı ve tanımına sahip olmanın kısmi optimizasyonundan daha etkili olduğunu göstermekte olup, burada sadece isme ve kat edilen mesafeye göre değişkenler vardır.

Ayrıca, Durum 1'de gidilen toplam mesafenin Durum 2'deki mesafeden çok daha uzak olduğunu görüyoruz ve bu mesafenin Zaman algoritmasını iki algoritma arasında gezdiğini düşünebiliriz . Vaka 1'in Vaka 2'den daha fazla işi vardır .

Bu, ASMher iki durumda da gösterilen talimatların kanıtlarından gözlemlenebilir . Bu davalar hakkında daha önce ifade edilenlerle birlikte, bu, Durum 1'de patronun her yineleme için tekrar geri dönmeden önce her ikisini de beklemesi Ave Cgeri dönmesi gerektiği gerçeğini açıklamaz A. Ayrıca son derece uzun zaman alıyorsa Aveya Bo zaman hem Bossçalışanların hem de diğer çalışanların çalışmayı bekleyen boşta oldukları gerçeğini hesaba katmaz .

In Durumunda 2 tek varlık boşta Bossişçi dönene kadar. Yani bunun bile algoritma üzerinde bir etkisi var.



OP'lerin Değiştirilmiş Soruları

DÜZENLEME: Sorunun önemsiz olduğu ortaya çıktı, çünkü davranış ciddi bir şekilde dizilerin (n) ve CPU önbelleğinin boyutlarına bağlıdır. Bu yüzden daha fazla ilgi varsa, soruyu yeniden ifade ederim:

Aşağıdaki grafikte beş bölge tarafından gösterildiği gibi farklı önbellek davranışlarına yol açan ayrıntılara ilişkin sağlam bir fikir verebilir misiniz?

Bu CPU'lar için benzer bir grafik sağlayarak CPU / önbellek mimarileri arasındaki farklara dikkat çekmek de ilginç olabilir.


Bu Sorular Hakkında

Şüphesiz gösterdiğim gibi, Donanım ve Yazılım devreye girmeden önce bile altta yatan bir sorun var.

Şimdi, bellek yönetimi ve önbellekleme ile birlikte, hepsi aşağıdakiler arasında entegre bir sistem kümesinde birlikte çalışan sayfa dosyaları vb.

  • The Architecture {Donanım, Bellenim, bazı Katıştırılmış Sürücüler, Çekirdekler ve ASM Komut Setleri}.
  • The OS{Dosya ve Bellek Yönetim sistemleri, Sürücüler ve Kayıt Defteri}.
  • The Compiler {Kaynak Kodun Çeviri Birimleri ve Optimizasyonları}.
  • Ve Source Codekendine özgü algoritma kümeleriyle bile .

Zaten biz bile herhangi İsteğe bağlı olmak ile herhangi bir makineye uygulamadan önce ilk algoritma dahilinde oluyor bir darboğaz olduğunu görebilirsiniz Architecture, OSve Programmable Languageikinci algoritmaya göre. Modern bir bilgisayarın içsel özelliklerini dahil etmeden önce zaten bir sorun vardı.


Bitiş Sonuçları

Ancak; bu yeni soruların önemli olmadığını söylemek değildir, çünkü kendileri ve sonuçta bir rol oynarlar. Prosedürleri ve genel performansı etkilerler ve bu, cevaplarını ve yorumlarını veren birçok grafik ve değerlendirmede açıkça görülür.

Eğer benzetmesi dikkat ödemişseniz Bossve iki işçi Ave Bgidip gelen paketleri almak zorunda kaldı Cve Dsırasıyla söz konusu iki algoritmaların matematiksel gösterimler dikkate; bilgisayar donanım ve yazılımlarının katılımı olmadan görebildiğinizden Case 2yaklaşık olarak 60%daha hızlıdır Case 1.

Bu algoritmalar bazı kaynak kodlara uygulandıktan, derlendi, optimize edildi ve işletim sistemi aracılığıyla belirli bir donanımda işlemlerini gerçekleştirmek için yürütüldükten sonra grafiklere ve çizelgelere baktığınızda, farklılıklar arasında biraz daha fazla bozulma görebilirsiniz. bu algoritmalarda.

Eğer Dataset oldukça küçüktür ilk başta bir fark o kadar feci görünmeyebilir. Ancak bu yana Case 1hakkındadır 60 - 70%daha yavaş Case 2biz zaman infaz farklılıklar açısından bu fonksiyonun büyüme bakabilirsiniz:

DeltaTimeDifference approximately = Loop1(time) - Loop2(time)
//where 
Loop1(time) = Loop2(time) + (Loop2(time)*[0.6,0.7]) // approximately
// So when we substitute this back into the difference equation we end up with 
DeltaTimeDifference approximately = (Loop2(time) + (Loop2(time)*[0.6,0.7])) - Loop2(time)
// And finally we can simplify this to
DeltaTimeDifference approximately = [0.6,0.7]*Loop2(time)

Bu yaklaşım, bu iki döngü arasında hem algoritmik olarak hem de yazılım optimizasyonlarını ve makine talimatlarını içeren makine işlemleri arasındaki ortalama farktır.

Veri kümesi doğrusal olarak büyüdüğünde, ikisi arasındaki zaman farkı da artar. Algoritma 1, algoritma 2'den daha fazla getirme özelliğine sahiptir; bu, ilk yinelemeden sonra her yineleme Bossarasında Ave Cher yineleme arasında maksimum mesafeyi ileri ve geri hareket etmesi gerektiğinde belirgindir ; Algoritma 2 ise bir kez ve daha sonra Bossseyahat etmek zorunda Akaldıktan sonra seyahat etmek Azorundadır. sadece bir kez maksimum mesafe geçerken Aiçin C.

BossAynı anda iki benzer şey yapmaya odaklanmaya çalışmak ve benzer ardışık görevlere odaklanmak yerine ileri geri hokkabazlık yapmaya çalışmak, günün sonunda iki kat daha fazla seyahat etmek ve çalışmak zorunda kaldığı için onu oldukça kızdırır. Bu nedenle, patronunuzun enterpolasyonlu bir darboğazına girmesine izin vererek durumun kapsamını kaybetmeyin, çünkü patronun eşi ve çocukları bunu takdir etmez.



Değişiklik: Yazılım Mühendisliği Tasarım İlkeleri

- Döngüler için yinelemeli Local Stackve Heap Allocatedyinelemeli hesaplamalar arasındaki farklar ve kullanımları, verimlilikleri ve etkililikleri arasındaki fark -

Yukarıda önerdiğim matematiksel algoritma, temelde yığın üzerinde tahsis edilen veriler üzerinde işlem yapan döngüler için geçerlidir.

  • Ardışık Yığın İşlemleri:
    • Döngüler, yığın çerçevesindeki tek bir kod bloğu veya kapsamı içinde veriler üzerinde yerel olarak işlemler gerçekleştiriyorsa, yine de bir çeşit uygulama olur, ancak bellek konumları genellikle sıralı oldukları ve seyahat edilen mesafe veya yürütme süresindeki farkın çok daha yakındır neredeyse ihmal edilebilir. Öbek içinde herhangi bir ayırma yapılmadığından, bellek dağılmaz ve bellek ram yoluyla getirilmez. Bellek genellikle sıralı ve yığın çerçevesi ve yığın işaretçisine göredir.
    • Yığın üzerinde ardışık işlemler yapıldığında, modern bir İşlemci yinelenen değerleri ve adresleri yerel önbellek kayıtları içinde tutarak adresleri önbelleğe alır. Buradaki işlemlerin veya talimatların süresi nano saniye düzenindedir.
  • Ardışık Yığın Tahsis Edilen İşlemler:
    • Yığın ayırmalarını uygulamaya başladığınızda ve işlemcinin CPU, Bus Denetleyici ve Ram modüllerinin mimarisine bağlı olarak ardışık aramalarda bellek adreslerini alması gerektiğinde, işlem veya yürütme süresi mikro milisaniye. Önbelleğe alınmış yığın işlemleriyle karşılaştırıldığında, bunlar oldukça yavaştır.
    • CPU'nun bellek adresini Ram'dan alması gerekir ve tipik olarak sistem veri yolundaki her şey CPU'nun içindeki dahili veri yollarına veya veri yollarına kıyasla yavaştır.

Bu nedenle, yığın üzerinde olması gereken verilerle çalışırken ve döngüler halinde dolaşırken, her veri kümesini ve karşılık gelen algoritmaları kendi döngüleri içinde tutmak daha verimlidir. Öbek üzerinde bulunan farklı veri kümelerinin birden çok işlemini tek bir döngüye koyarak ardışık döngüleri etkisizleştirmeye kıyasla daha iyi optimizasyonlar elde edersiniz.

Bunu sıkça önbelleğe alındıkları için yığın üzerinde olan verilerle yapmak uygundur, ancak bellek adresinin her yinelemeyi sorgulaması gereken veriler için uygun değildir.

Yazılım Mühendisliği ve Yazılım Mimarisi Tasarımı devreye giriyor. Verilerinizi nasıl düzenleyeceğinizi, verilerinizi ne zaman önbelleğe alacağınızı, verilerinizi ne zaman öbek üzerinde tahsis edeceğinizi, algoritmalarınızı nasıl tasarlayıp uygulayacağınızı ve bunları ne zaman ve nerede arayacağınızı bilme yeteneğidir.

Aynı veri kümesine ait aynı algoritmaya sahip olabilirsiniz, ancak yalnızca O(n)çalışma sırasında algoritmanın karmaşıklığından görülen yukarıdaki sorun nedeniyle, yığın varyantı için bir uygulama tasarımı ve diğeri yığın tahsisli varyantı için isteyebilirsiniz. yığın ile.

Yıllar boyunca fark ettiğim kadarıyla, birçok insan bu gerçeği dikkate almaz. Belirli bir veri kümesi üzerinde çalışan bir algoritma tasarlama eğiliminde olacaklar ve veri kümesinin yığın üzerinde yerel olarak önbelleğe alınmasına bakılmaksızın veya yığınta tahsis edildiklerinden bağımsız olarak kullanacaklardır.

Gerçek optimizasyon istiyorsanız, evet kod çoğaltma gibi görünebilir, ancak genelleştirmek için aynı algoritmanın iki varyantına sahip olmak daha verimli olacaktır. Biri yığın işlemleri için, diğeri yinelemeli döngülerde gerçekleştirilen yığın işlemleri için!

İşte sahte bir örnek: İki basit yapı, bir algoritma.

struct A {
    int data;
    A() : data{0}{}
    A(int a) : data{a}{} 
};
struct B {
    int data;
    B() : data{0}{}
    A(int b) : data{b}{}
}                

template<typename T>
void Foo( T& t ) {
    // do something with t
}

// some looping operation: first stack then heap.

// stack data:
A dataSetA[10] = {};
B dataSetB[10] = {};

// For stack operations this is okay and efficient
for (int i = 0; i < 10; i++ ) {
   Foo(dataSetA[i]);
   Foo(dataSetB[i]);
}

// If the above two were on the heap then performing
// the same algorithm to both within the same loop
// will create that bottleneck
A* dataSetA = new [] A();
B* dataSetB = new [] B();
for ( int i = 0; i < 10; i++ ) {
    Foo(dataSetA[i]); // dataSetA is on the heap here
    Foo(dataSetB[i]); // dataSetB is on the heap here
} // this will be inefficient.

// To improve the efficiency above, put them into separate loops... 

for (int i = 0; i < 10; i++ ) {
    Foo(dataSetA[i]);
}
for (int i = 0; i < 10; i++ ) {
    Foo(dataSetB[i]);
}
// This will be much more efficient than above.
// The code isn't perfect syntax, it's only psuedo code
// to illustrate a point.

Yığın varyantlarına karşı yığın varyantları için ayrı uygulamalar yaparak bahsettiğim şey budur. Algoritmaların kendileri çok fazla önemli değil, bunları kullanacağınız döngü yapıları.


Bu yanıtı yayınladığımdan bu yana bir süre geçti, ancak bunu anlamaya yardımcı olabilecek hızlı bir yorum da eklemek istedim: Boss ile for döngüsü olarak veya bir döngü aracılığıyla özet veya yinelemeler olarak, bu patronu kapsam ve yığın değişkenlerini ve for döngülerinin bellek adreslemesini yöneten Yığın Çerçeve ve Yığın İşaretçisi arasındaki kombinasyon olarak düşünün.
Francis Cugler

@PeterMortensen Orijinal cevabımı hafifçe değiştirerek tavsiyenizi dikkate aldım. Önerdiğin bu olduğuna inanıyorum.
Francis Cugler

2

Eski C ++ ve optimizasyonlar olabilir. Bilgisayarımda neredeyse aynı hızı elde ettim:

Bir döngü: 1.577 ms

İki döngü: 1.507 ms

Visual Studio 2015'i, 16 GB RAM'e sahip bir E5-1620 3,5 GHz işlemcide çalıştırıyorum.

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.