Stavo sperimentando con i set di istruzioni AVX -AVX2 per vedere le prestazioni dello streaming su array consecutivi. Quindi ho sotto l'esempio, dove faccio memoria di base leggere e memorizzare.Accesso alla memoria Haswell
#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;
}
E dopo la compilazione con g ++ - 4.9 -ggdb -march = core-AVX2 -std = C++ 11 struct_of_arrays.cpp -O3 -o struct_of_arrays
vedo abbastanza buona istruzione per prestazioni del ciclo e tempi, per il benchmark 4000. Tuttavia, una volta aumentata la dimensione del benchmark a 5000, vedo che le istruzioni per ciclo calano in modo significativo e anche i salti di latenza. Ora la mia domanda è, anche se posso vedere che il degrado delle prestazioni sembra essere correlato alla cache L1, non posso spiegare perché questo accade così all'improvviso.
Per dare un quadro più chiaro, se corro perf con la dimensione di riferimento 4000 e 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 |
Quindi la mia domanda è: perché questo impatto sta accadendo, considerando Haswell dovrebbe essere in grado di erogare 2 * 32 byte per leggi e 32 byte memorizzano ogni ciclo?
EDIT 1
mi sono reso conto con questo codice gcc elimina elegantemente accessi al myData.a dal momento che è impostato a 0. Per evitare questo ho fatto un altro punto di riferimento che è leggermente diverso, in cui A è impostato in modo esplicito .
#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;
}
Secondo esempio avrà un array in fase di lettura e altre serie in fase di scrittura. E questo produce output seguente perf per diverse dimensioni:
| 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 |
Ancora stesso schema è visto come sottolineato nella risposta, all'aumentare dati data set di dimensioni non rientra in L1 e L2 più diventa collo di bottiglia. Ciò che è anche interessante è che il prefetching non sembra essere d'aiuto e L1 manca aumenta considerevolmente. Anche se, mi aspetto di vedere almeno il 50 percento di hit rate considerando che ogni riga di cache introdotta in L1 per read sarà un successo per il secondo accesso (64 byte di cache line 32 byte letti con ogni iterazione). Tuttavia, una volta che il set di dati è stato trasferito su L2, sembra che il tasso di successo L1 scenda al 2%. Considerando che gli array non si sovrappongono realmente con la dimensione della cache L1, ciò non dovrebbe dipendere dai conflitti nella cache. Quindi questa parte non ha ancora senso per me.
+1. L'unica cosa che vorrei aggiungere è che su ogni piattaforma x86 che ho visto, una doppia è di 8 byte. –
In effetti hai ragione riguardo la scrittura di back e il modo in cui consumano la larghezza di banda se non sono in L1. È un po 'deludente non poter sfruttare la potenza dell'unità di elaborazione se i dati non sono in L1 (il che sarà quasi sempre il caso di utilizzo di streaming più grande di L1). – edorado
Questo è il motivo per cui gli algoritmi di prestazioni critiche spesso suddividono il loro working set in sottoinsiemi che possono adattarsi alle cache più piccole (vedere ad esempio le tecniche di tiling della cache). Secondo l'articolo L2, la larghezza di banda è stata anche aumentata rispetto alle vecchie CPU, immagino che sia difficile raggiungere i miglioramenti L1 – Leeor