我正在試驗AVX-AVX2指令集以查看連續陣列上的流式傳輸性能。所以我有下面的例子,我做基本的內存讀取和存儲。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;
}
並與 編譯後克++ - 4.9 -ggdb -march =芯AVX2 -std = C++ 11 struct_of_arrays.cpp -O3 -o struct_of_arrays
我看每循環性能相當不錯指令和時間,對於基準尺寸4000.然而,一旦我將基準尺寸增加到5000,我看到每個週期的指令顯着下降,並且延遲跳躍。 現在我的問題是,雖然我可以看到性能下降 似乎與L1緩存有關,但我無法解釋爲什麼會這麼突然發生。
爲了讓更多的有識之士,如果我跑PERF與基準尺寸4000和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 |
所以我的問題是,爲什麼這種影響正在發生的事情,考慮的Haswell應該能夠提供2 * 32個字節的讀取,每個週期存儲32個字節?
EDIT 1
我與此代碼的gcc實現巧妙消除訪問,因爲它被設置爲0爲了避免這種情況的myData.a我沒有另一個基準,這是稍有不同,其中一個顯式設置。
#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;
}
第二個例子將有一個數組正在被讀取,另一個數組正在被寫入。 而這一次產生以下輸出PERF的不同尺寸:如在回答中指出,隨着 數據集大小的數據不適合於再L1和L2變爲瓶頸
| 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 |
再次相同的模式看出。 也是有趣的是,預取似乎沒有幫助,L1錯過 大大增加。雖然,我認爲考慮到讀入L1的每個緩存行至少有50%的命中率,對於第二次訪問(64字節緩存行32字節在每次迭代中讀取)將是命中 。但是,一旦數據集溢出到L2,似乎L1命中率下降到2%。考慮到數組並不真正與L1緩存大小重疊,這應該不是因爲緩存衝突。所以這部分對我來說仍然沒有意義。
+1。我唯一要補充的是,在我見過的每一個x86平臺上,一個double都是8個字節。 –
事實上,如果他們不在L1中,你是否正確回寫以及如何消費帶寬。如果數據不在L1中,那麼不能利用處理單元的能力是有點令人失望的(對於大於L1的任何流式使用情況,情況幾乎總是如此)。 – edorado
這就是爲什麼性能關鍵算法經常將他們的工作集分成可以適應較小緩存的子集的原因(請參閱例如緩存切片技術)。根據文章L2帶寬也增加了相比較老的CPU,我想這只是很難趕上L1的改進 – Leeor