Rastgele okumaların paralelleştirilmesi iyi çalışıyor gibi görünüyor - neden?


18

Aşağıdaki çok basit bilgisayar programını düşünün:

for i = 1 to n:
    y[i] = x[p[i]]

Burada ve y , baytların n- elementli dizileridir ve p , n- elementli bir kelime dizisidir. Burada n büyüktür, örneğin, n = 2 31 (böylece verilerin sadece ihmal edilebilir bir kısmı herhangi bir önbellek türüne sığar).xynpnnn=231

1 ile n arasında eşit olarak dağılmış rastgele sayılardan oluştuğunu varsayın .p1n

Modern donanım açısından, bu aşağıdakiler anlamına gelmelidir:

  • okuma ucuz (ardışık okuma) olduğup[i]
  • okumak çok pahalıdır (rastgele okumalar; neredeyse tüm okumalar önbellek özledimidir; her bir baytı ana bellekten almak zorundayız)x[p[i]]
  • yazma ucuz (sıralı yazma) 'dir.y[i]

Ve bu gerçekten gözlemlediğim şey. Program sadece sıralı okuma ve yazma yapan bir programla karşılaştırıldığında çok yavaştır. Harika.

Şimdi soru geliyor: Bu program modern çok çekirdekli platformlarda ne kadar iyi paralellik gösteriyor ?


Benim hipotezim, bu programın iyi paralellik göstermediğiydi. Sonuçta, darboğaz ana hafızadır. Tek bir çekirdek zamanının çoğunu sadece ana bellekten bazı verileri beklemek için harcıyor.

Ancak, bu oldu değil ben darboğaz operasyon bu tür bazı algoritmalar denemeye başladığında ne gözlenen!

Basit for-loop'u bir OpenMP paralel for-loop ile değiştirdim (özünde, sadece aralığını daha küçük parçalara böler ve bu parçaları paralel olarak farklı CPU çekirdeklerinde çalıştırır).[1,n]

Düşük seviye bilgisayarlarda, hızlanmalar gerçekten küçüktü. Ancak üst düzey platformlarda mükemmel lineer hızlanma elde ettiğime şaşırdım. Bazı somut örnekler (kesin zamanlamalar biraz kapalı olabilir, çok fazla rastgele varyasyon var; bunlar sadece hızlı deneylerdi):

  • 2 x 4 çekirdekli Xeon (toplam 8 çekirdek): tek iş parçacıklı versiyona kıyasla faktör 5-8 hızlanma.

  • 2 x 6 çekirdekli Xeon (toplam 12 çekirdek): tek iş parçacıklı versiyona göre faktör 8-14 hızlanma.

Şimdi bu tamamen beklenmedik bir şeydi. Sorular:

  1. Tam olarak bu tür bir program neden bu kadar iyi paralellik gösteriyor ? Donanımda ne olur? (Şu anki tahminim bu satırlar boyunca bir şey: farklı iş parçacığından gelen rastgele okumalar "ardışık düzenlenmiş" ve bunlara ortalama yanıt alma oranı tek bir iş parçacığından çok daha yüksek.)

  2. Öyle mi birden konuları ve çoklu çekirdek kullanmak için gerekli herhangi bir kat hızlanma elde etmek? Eğer ana bellek ve CPU arasındaki arayüzde bir çeşit boru hattı gerçekleşirse, tek iş parçacıklı bir uygulama ana belleğe yakında , x [ p [ i + 1 ] ] , ... ve bilgisayar ilgili önbellek satırlarını ana bellekten almaya başlayabilir mi? Prensipte bu mümkün ise, pratikte bunu nasıl başarabilirim?x[p[i]]x[p[i+1]]

  3. Bu tür programları analiz etmek (ve performansın doğru tahminlerini yapmak) için kullanabileceğimiz doğru teorik model nedir ?


Düzenleme: Şimdi burada bazı kaynak kodu ve karşılaştırma sonuçları var: https://github.com/suomela/parallel-random-read

Bazı basketbol sahası örnekleri ( ):n=232

  • yak. Tek bir iş parçacığı ile yineleme başına 42 ns (rastgele okuma)
  • yak. 12 çekirdek ile yineleme başına 5 ns (rastgele okuma).

Yanıtlar:


9

pnpnpp

Şimdi, bellek sorunlarını ele alalım. Üst düzey Xeon tabanlı düğümünüzde gerçekte gözlemlediğiniz süper doğrusal hızlanma aşağıdaki gibi doğrulanır.

nn/pp

n=231

İkinci sorunuzla ilgili olarak, mevcut mimariler, önbellek satırlarını çıkararak ve geçici ve aralıklı veri konumlarından yararlanmak için gerektiğinde değiştirerek verileri önceden alıyor. Ancak bu, 2048 Mbayt veri işleyen tek bir çekirdek için yeterli olmayacaktır. Kısıtlarsanızn

Son olarak, QSM'nin (Queuing Shared Memory) yanı sıra , paylaşılan belleğe erişim tartışmasını da dikkate alarak başka bir teorik paralel modelin farkında değilim (sizin durumunuzda, ana bellek çekirdekler arasında paylaşılır ve önbellek her zaman çekirdekler arasında da paylaşılır). Her neyse, model ilginç olsa da, büyük başarı elde etmedi.


1
Ayrıca, her çekirdek, belirli bir zamanda işlemde 10 x [] yük gibi az ya da çok sabit miktarda bellek seviyesi paralelliği sağladığından da bakmaya yardımcı olabilir. Paylaşılan L3'te% 0.5'lik bir isabet şansı ile, tek bir iş parçacığının 0.995 ** 10 (% 95 +) tüm bu yüklerin ana hafıza cevabını beklemesini gerektirme şansı olacaktır. Toplam 60 x [] beklemede okuma sağlayan 6 çekirdekle, L3'te en az bir okumanın çarpma olasılığı yaklaşık% 26'dır. Buna ek olarak, ne kadar MLP olursa, bellek denetleyicisi gerçek bant genişliğini artırmak için erişimi o kadar zamanlayabilir.
Paul A. Clayton

5

Kendimi __builtin_prefetch () denemeye karar verdim. Başkalarının makinelerinde test etmek istemesi durumunda burada yanıt olarak gönderiyorum. Sonuçlar Jukka'nın açıkladığı şeye yakındır: Öndeki 20 öğeyi önceden getirirken, 0 öğeyi önceden getirirken çalışma süresinde yaklaşık% 20 azalma.

Sonuçlar:

prefetch =   0, time = 1.58000
prefetch =   1, time = 1.47000
prefetch =   2, time = 1.39000
prefetch =   3, time = 1.34000
prefetch =   4, time = 1.31000
prefetch =   5, time = 1.30000
prefetch =   6, time = 1.27000
prefetch =   7, time = 1.28000
prefetch =   8, time = 1.26000
prefetch =   9, time = 1.27000
prefetch =  10, time = 1.27000
prefetch =  11, time = 1.27000
prefetch =  12, time = 1.30000
prefetch =  13, time = 1.29000
prefetch =  14, time = 1.30000
prefetch =  15, time = 1.28000
prefetch =  16, time = 1.24000
prefetch =  17, time = 1.28000
prefetch =  18, time = 1.29000
prefetch =  19, time = 1.25000
prefetch =  20, time = 1.24000
prefetch =  19, time = 1.26000
prefetch =  18, time = 1.27000
prefetch =  17, time = 1.26000
prefetch =  16, time = 1.27000
prefetch =  15, time = 1.28000
prefetch =  14, time = 1.29000
prefetch =  13, time = 1.26000
prefetch =  12, time = 1.28000
prefetch =  11, time = 1.30000
prefetch =  10, time = 1.31000
prefetch =   9, time = 1.27000
prefetch =   8, time = 1.32000
prefetch =   7, time = 1.31000
prefetch =   6, time = 1.30000
prefetch =   5, time = 1.27000
prefetch =   4, time = 1.33000
prefetch =   3, time = 1.38000
prefetch =   2, time = 1.41000
prefetch =   1, time = 1.41000
prefetch =   0, time = 1.59000

Kod:

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

void cracker(int *y, int *x, int *p, int n, int pf) {
    int i;
    int saved = pf;  /* let compiler optimize address computations */

    for (i = 0; i < n; i++) {
        __builtin_prefetch(&x[p[i+saved]]);
        y[i] += x[p[i]];
    }
}

int main(void) {
    int n = 50000000;
    int *x, *y, *p, i, pf, k;
    clock_t start, stop;
    double elapsed;

    /* set up arrays */
    x = malloc(sizeof(int)*n);
    y = malloc(sizeof(int)*n);
    p = malloc(sizeof(int)*n);
    for (i = 0; i < n; i++)
        p[i] = rand()%n;

    /* warm-up exercise */
    cracker(y, x, p, n, pf);

    k = 20;
    for (pf = 0; pf < k; pf++) {
        start = clock();
        cracker(y, x, p, n, pf);
        stop = clock();
        elapsed = ((double)(stop-start))/CLOCKS_PER_SEC;
        printf("prefetch = %3d, time = %.5lf\n", pf, elapsed);
    }
    for (pf = k; pf >= 0; pf--) {
        start = clock();
        cracker(y, x, p, n, pf);
        stop = clock();
        elapsed = ((double)(stop-start))/CLOCKS_PER_SEC;
        printf("prefetch = %3d, time = %.5lf\n", pf, elapsed);
    }

    return 0;
}

4
  1. DDR3 erişimi gerçekten ardışık düzende. http://www.eng.utah.edu/~cs7810/pres/dram-cs7810-protocolx2.pdf 20 ve 24 numaralı slaytlar, kanallı okuma işlemleri sırasında bellek veriyolunda neler olduğunu gösterir.

  2. (kısmen yanlış, aşağıya bakın) CPU mimarisi önbellek önceden getirmeyi destekliyorsa birden fazla iş parçacığı gerekli değildir. Modern x86 ve ARM ve diğer birçok mimarinin açık bir ön alma talimatı vardır. Birçoğu ek olarak bellek erişimlerindeki desenleri tespit etmeye ve ön getirmeyi otomatik olarak yapmaya çalışır. Yazılım desteği derleyiciye özgüdür, örneğin GCC ve Clang, açık ön alma için içsel __builtin_prefech () özelliğine sahiptir.

Intel tarzı hiper iş parçacığı, zamanlarının çoğunu önbellek özlemlerini beklerken geçiren programlar için çok iyi çalışıyor gibi görünüyor. Deneyimlerime göre, hesaplama yoğun iş yükünde hızlanma fiziksel çekirdek sayısının çok az üzerine çıkıyor.

DÜZENLEME: Nokta 2'de yanılmışım. Ön getirme tek çekirdekli bellek erişimini optimize edebilirken, birden fazla çekirdeğin birleştirilmiş bellek bant genişliği tek çekirdekli bant genişliğinden daha fazla gibi görünüyor. Ne kadar büyük, CPU'ya bağlıdır.

Donanım prefetcher ve diğer optimizasyonlar birlikte kıyaslamayı çok zorlaştırır. Açık ön getirmenin performans üzerinde çok görünür veya var olmayan bir etkiye sahip olduğu durumlar oluşturmak mümkündür, bu kriter ikincisidir.


__builtin_prefech çok umut verici geliyor. Ne yazık ki, hızlı deneylerimde tek iş parçacığı performansına pek yardımcı olmadı (<% 10). Bu tür uygulamalarda ne kadar büyük iyileştirmeler beklemeliyim?
Jukka Suomela

Daha fazlasını bekliyordum. Ön getirmenin DSP ve oyunlarda önemli bir etkisi olduğunu bildiğim için kendimi denemek zorunda kaldım. Tavşan deliği daha derine
Juhani Simola

İlk denemem, bir dizide saklanan sabit bir rasgele sıra oluşturmak, ardından önceden getirilmiş ve getirmeden bu sırayla yinelemekti ( gist.github.com/osimola/7917602 ). Bu, Core i5'te% 2 civarında bir fark yarattı. Önceden getirme ya hiç çalışmıyor ya da donanım tahmincisi dolaylı olarak anlaşılıyor.
Juhani Simola

1
Bu nedenle, test için ikinci girişim ( gist.github.com/osimola/7917568 ), belleğe sabit bir rastgele tohum tarafından üretilen sırayla erişir. Bu kez, önceden getirme sürümü, önceden getirme işleminden yaklaşık 2 kat daha hızlı ve 1 adım ileride önceden getirmeden 3 kat daha hızlıdır. Önceden getirme sürümünün bellek erişimi başına getirme öncesi sürümden daha fazla hesaplama yaptığını unutmayın.
Juhani Simola

Bu makineye bağlı gibi görünüyor. Aşağıdaki Pat Morin kodunu denedim (itibara sahip olmadığım için bu yazıya yorum yapamam) ve sonucum farklı prefetch değerleri için% 1.3 içinde.
Juhani Simola
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.