2013-10-27 45 views
17

Ardışık dizilerde akış performansını görmek için AVX -AVX2 komut kümeleriyle denemeler yapıyordum. Bu yüzden temel hafızayı okuduğum ve sakladığım bir örneğim var. Haswell bellek erişimi

#include <iostream> 
#include <string.h> 
#include <immintrin.h> 
#include <chrono> 
const uint64_t BENCHMARK_SIZE = 5000; 

typedef struct alignas(32) data_t { 
    double a[BENCHMARK_SIZE]; 
    double c[BENCHMARK_SIZE]; 
    alignas(32) double b[BENCHMARK_SIZE]; 
} 
data; 

int main() { 
    data myData; 
    memset(&myData, 0, sizeof(data_t)); 

    auto start = std::chrono::high_resolution_clock::now(); 

    for (auto i = 0; i < std::micro::den; i++) { 
    for (uint64_t i = 0; i < BENCHMARK_SIZE; i += 1) { 
     myData.b[i] = myData.a[i] + 1; 
    } 
    } 
    auto end = std::chrono::high_resolution_clock::now(); 
    std::cout << (end - start).count()/std::micro::den << " " << myData.b[1] 
      << std::endl; 
} 

ve ile derleme sonra

g ++ - 4.9 -ggdb -march = çekirdek AVX2 -std = C++ 11 struct_of_arrays.cpp -O3 -o struct_of_arrays

I döngüsü performansına göre oldukça iyi bir talimat bakınız ve zamanlama, benek boyutu 4000 için. Ancak, benchmark büyüklüğünü 5000'e yükselttiğimde, devir başına talimatın önemli ölçüde düştüğünü ve ayrıca gecikme atladığını görüyorum. Şimdi sorum şu ki, bu performans düşüşünü L1 önbelleği ile ilgili görebiliyor gibi görsem de, bunun neden bu kadar aniden olduğunu açıklayamıyorum. Ben Benchmark büyüklüğü 4000 ile perf çalıştırırsanız

, daha fazla fikir vermek için, ve bu darbe neden oluyor 5000

| Event        | Size=4000 | Size=5000 | 
|-------------------------------------+-----------+-----------| 
| Time        | 245 ns | 950 ns | 
| L1 load hit       | 525881 | 527210 | 
| L1 Load miss      |  16689 |  21331 | 
| L1D writebacks that access L2 cache | 1172328 | 623710387 | 
| L1D Data line replacements   | 1423213 | 624753092 | 

Benim soru, Haswell dikkate 2 * 32 bayt sunma yeteneğine olmalıdır edilir Okumak ve 32 bayt her döngü saklamak?

Bu kod gcc ile gerçekleştirilen 1

DÜZENLEME akıllıca ortadan kaldırır o Bunu önlemek için 0'a ayarlanır beri myData.a için erişir yaptım, bir açık ayarlanır nerede biraz farklı olan başka kriter .

#include <iostream> 
#include <string.h> 
#include <immintrin.h> 
#include <chrono> 
const uint64_t BENCHMARK_SIZE = 4000; 

typedef struct alignas(64) data_t { 
    double a[BENCHMARK_SIZE]; 
    alignas(32) double c[BENCHMARK_SIZE]; 

    alignas(32) double b[BENCHMARK_SIZE]; 

} 
data; 

int main() { 
    data myData; 
    memset(&myData, 0, sizeof(data_t)); 
    std::cout << sizeof(data) << std::endl; 
    std::cout << sizeof(myData.a) << " cache lines " << sizeof(myData.a)/64 
      << std::endl; 
    for (uint64_t i = 0; i < BENCHMARK_SIZE; i += 1) { 
    myData.b[i] = 0; 
    myData.a[i] = 1; 
    myData.c[i] = 2; 
    } 

    auto start = std::chrono::high_resolution_clock::now(); 
    for (auto i = 0; i < std::micro::den; i++) { 
    for (uint64_t i = 0; i < BENCHMARK_SIZE; i += 1) { 
     myData.b[i] = myData.a[i] + 1; 
    } 
    } 
    auto end = std::chrono::high_resolution_clock::now(); 
    std::cout << (end - start).count()/std::micro::den << " " << myData.b[1] 
      << std::endl; 
} 

İkinci örnekte bir dizi okunacak ve diğer dizi yazılacak. ve bu farklı boyutları için perf çıkış aşağıdaki üretir: artık L1 sığmayan veri kümesi boyutu artan veriler ve L2 darboğaz ile cevap belirttiği gibi

| Event   | Size=1000 | Size=2000 | Size=3000 | Size=4000  | 
|----------------+-------------+-------------+-------------+---------------| 
| Time   | 86 ns  | 166 ns  | 734 ns  | 931 ns  | 
| L1 load hit | 252,807,410 | 494,765,803 | 9,335,692 | 9,878,121  | 
| L1 load miss | 24,931  | 585,891  | 370,834,983 | 495,678,895 | 
| L2 load hit | 16,274  | 361,196  | 371,128,643 | 495,554,002 | 
| L2 load miss | 9,589  | 11,586  | 18,240  | 40,147  | 
| L1D wb acc. L2 | 9,121  | 771,073  | 374,957,848 | 500,066,160 | 
| L1D repl.  | 19,335  | 1,834,100 | 751,189,826 | 1,000,053,544 | 

Yine aynı model görülür. nedir, aynı zamanda, prefetching'in yardım etmeyeceği ve L1'in kaçırdığı 'un önemli ölçüde arttığı da ilginçtir. Her ne kadar okuma için L1'e getirilen her önbellek çizgisi dikkate alındığında en az yüzde 50 isabet oranı görmeyi beklerdim, ikinci erişim için bir isabetli olacaktır (her bir yineleme ile 64 bayt önbellek satırı 32 bayt okunur). Ancak, bir kez veri seti L2'ye döküldüğünde, L1 vuruş oranı% 2'ye düşüyor. Diziler göz önüne alındığında, L1 önbellek boyutu ile gerçekten örtüşmezler, bu önbellek çakışmalarından kaynaklanmamalıdır. Yani bu kısım hala bana mantıklı gelmiyor.

cevap

18

Yönetici özeti:
Farklı önbellek seviyeleri o kadar farklı büyüklükte veri setleri büyük ölçüde performansını etkileyebilir sahip aynı temel iş yükü için farklı zirve bant genişlikleri devam edebilir.

Uzun açıklama:
O Haswell, this article göre için örneğin göz önüne alındığında çok şaşırtıcı değil

2 yükleri ve çevrim

başına 1 mağaza devam edebilir ama bu sadece L1 için başvuruda söylenir. Eğer üzerinde okursanız, L2

veri veya talimat önbelleği her döngü

Bir yük ve yineleme başına bir mağaza gerektiğinden

tam 64B hattı sağlayabilir görüyoruz veri setinin L1'de bulunması L1 bant genişliğinin keyfini çıkarmanıza ve muhtemelen çevrim başına devir işlem hacmine ulaşmanıza izin verirken, veri setinin L2'ye yayılması daha uzun süre beklemenizi zorlayacaktır. Bu, sisteminizde ne kadar büyük çift olduğuna bağlı, ancak sonuçlarınız muhtemelen 32bit olduğunu, yani 4000 * 2 dizinin * 4 byte = 32k olduğunu, tam olarak L1 boyutunun ve 5000'in de bunu aştığını gösteriyor.

  1. L1-writebacks: makalede sahip ek bir ceza vardır writebacks söz vermediğini unutmayın

    Şimdi bir sonraki önbellek düzeyine aşan başladıktan sonra gerçekleşmesi iki şey vardır bant genişliği açısından ödeme yapmak (perf çıkışınızdan görülebileceği gibi - biraz dik görünmesine rağmen). Verilerin L1'de tutulması demek, herhangi bir tahliyeyi yapmak zorunda kalmamanız anlamına gelirken, L2'de bazı veriler olması, L2'den okunan her satırın L1'den var olan bir çizgiyi atmak zorunda kalacağı anlamına gelir. Kodunuz ve açık yazarlar gerektirir. Bu işlemler, yineleme başına kullandığınız iki veri elemanının değerlerini okumaya devam etmek zorunda kalacaktı - hatırlamak gerekirse, hattın bir kısmı kullanılmadığından ve birleştirmeyi gerektirdiğinden, mağazanın eski verileri de okuması gerekiyor.

  2. Önbellek yedek politikası - o zaman, ilk ilişkisel bir şekilde doldurulması önbellek bir LRU şema kullanılarak büyük olasılıkla ilişkisel kurmak ve olduğundan unutmayın ve seri Dizilerinize üzerinden gitmek beri, önbellek kullanımı desen muhtemelen olurdu ikinci yoldan devam etmek, vb. - son yolu doldurduğunuz zaman, eğer L2'de hala gerekli olan veri varsa (daha büyük veri setinde), büyük olasılıkla tüm satırları ilk yoldan tahliye etmiş olursunuz. En az kullanılmış olanlardır, bunun yanında bir sonraki kullanacağınız şeyleri de ifade eder. Bu, önbellekten daha büyük veri kümeleriyle LRU'nun dezavantajı. Tek bir yol (L1 önbellek 1/8) en az boyutuna göre önbellek boyutunu aşan bir kez performans düşüşü nedeniyle bu erişim desenine, çok ani neden

açıklıyor.

Mükemmel sonuçlarla ilgili son bir yorum - L1 isabet oranının 5000 elemanlık durum için güzel bir raunduna düşmesini bekledim. Ancak, HW önyükleme, gerçek veri okumalarının önüne geçtiğinde L1'de hala vuruyormuş gibi görünebilir. Verileri getirmek için bu ön yüklemeleri beklemek zorunda kalıyorsunuz ve daha da önemlisi bant genişliğini ölçtüğünüz için - gerçek yükler/mağazalar ile aynı bant genişliğini alıyorlar, ancak bunlar mükemmel bir şekilde hesaba katılmıyor. L1'in her yerinde vurdun. En azından benim en iyi tahminim budur - ön transferleri devre dışı bırakıp tekrar ölçerek kontrol edebilirsiniz (bu tavsiyeyi çok sık görüyorum gibi görünüyorum, böyle bir sürüklenme olduğum için üzgünüm).


DÜZENLEME 1 (aşağıdaki sizin)

çift büyüklüğünde sır çözer ortadan tadını yaklaşık

büyük yakalamak - bu 64-bit gerçekten, bu yüzden 4000 elemanları ya da 2 dizilerin bir dizi ya da 2000 elemanın her biri (düzeltmenizden sonra) L1'e sığabilecek kadardır. Şimdi dökülme 3000 elementte gerçekleşir. L1 isabet oranı düşüktür, çünkü L1, 2 farklı akışınızın önüne geçmek için yeterince ön yükleme yapamaz.

Her bir yükün 2 yineleme için 64 baytlık bir çizgi getireceği beklentisi ile ilgili olarak - oldukça ilginç bir şey görüyorum - bellek biriminden (L1 vuruşları + L1 kayıpları) verilen yüklerin sayısını toplarsanız, 2000 elemanının 1000 elemandan neredeyse tam olarak 2x olduğunu göreceksiniz, ancak 3000 ve 4000 vakaları sırasıyla 3x ve 4x değil, yarı yarıya. Spesifik olarak, dizi başına 3000 eleman ile 2000 elementten daha az erişime sahip olursunuz!
Bu, bellek biriminin her 2 yükü tek bir bellek erişiminde birleştirebileceğinden şüphelenir, ancak yalnızca L2 ve ötesine giderken. Bunu düşündüğünüzde, bu satır için zaten bir tane beklemeniz gerekiyorsa, L2'yi aramak için başka bir erişim sağlamak için bir neden yoktur ve bu düzeydeki daha düşük bant genişliğini azaltmak için uygulanabilir bir yoldur. Sanırım bir sebepten ötürü ikinci yük bir L1 araması olarak sayılmıyor ve görmek istediğiniz isabet oranını desteklemiyor (kaç yükün yürütmeyi geçtiğini gösteren sayaçları kontrol edebilirsiniz) Muhtemelen doğru olabilir. Bu sadece bir önsezidir, sayacın nasıl tanımlandığından emin değilim, ancak gördüğümüz erişim sayısına uygun.

+1

+1. Ekleyeceğim tek şey, gördüğüm her x86 platformunda, bir çiftin 8 bayt olmasıdır. –

+0

Gerçekten de, arka planları ve L1'de olmadıklarında bant genişliğini nasıl kullandıklarını bilmek hakkınız vardır. Veriler L1'de değilse (L1'den daha büyük herhangi bir gerçek zamanlı kullanım durumunda hemen hemen her zaman olacaksa) işlem ünitesinin gücünü kullanamamak hayal kırıklığı yaratıyor. – edorado

+1

Bu nedenle, performans kritik algoritmaları çalışma kümelerini genellikle daha küçük önbelleklere sığabilen alt kümelere ayırır (örneğin, önbellek döşeme tekniklerine bakın). Makaleye göre L2 bant genişliği eski CPU'lara göre de artmıştı, sanırım L1 geliştirmeleri ile yetinmek zor oluyor. – Leeor