Я экспериментировал с наборами инструкций AVX -AVX2, чтобы увидеть производительность потоковой передачи по последовательным массивам. Итак, у меня есть пример, где я читаю и сохраняю основную память.
#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;
}
И после компиляции с g++ - 4.9 -ggdb -march = core-avx2 -std = С++ 11 struct_of_arrays.cpp -O3 -o struct_of_arrays
Я вижу неплохую инструкцию для каждого цикла производительности и таймингов для эталонного размера 4000. Однако, как только я увеличиваю размер теста до 5000, я вижу, что инструкция за такт значительно падает, а также латентные прыжки. Теперь мой вопрос: хотя я вижу, что ухудшение производительности похоже, связано с кешем L1, я не могу объяснить, почему это происходит так внезапно.
Чтобы дать больше понимания, если я запустил perf с Benchmark размером 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 байта хранить каждый цикл?
РЕДАКТИРОВАТЬ 1
Я понял с помощью этого кода gcc решительно устраняет обращения к myData.a, так как он установлен в 0. Чтобы этого избежать, я сделал еще один тест, который немного отличается, где явно задано значение.
#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;
}
Второй пример будет иметь один считываемый массив и другой массив. И этот продукт производит следующий выход для разных размеров:
| 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, а L2 становится узким местом. Что также интересно, что предварительная выборка, похоже, не помогает, а L1 пропускает значительно увеличивается. Хотя, я ожидал бы увидеть как минимум 50-процентный рейтинг хитов, учитывая, что каждая строка кэша, привезенная в L1, для чтения будет хитом для второго доступа (64 байта строки байта 32 байта считывается с каждой итерацией). Однако, как только набор данных переливается на L2, кажется, что L1 снизился до 2%. Учитывая, что массивы не перекрываются с размером кеша L1, это не должно быть из-за конфликтов кэша. Поэтому эта часть мне все же не имеет смысла.