연속 배열에서 스트리밍 성능을 보려면 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;
}
그리고 로 컴파일 후
g ++ - 4.9 -ggdb -march = 코어 AVX2 -std = C++ 11 struct_of_arrays.cpp -03 -o struct_of_arrays내가 사이클 성능에 따라 꽤 좋은 내용을 참조하십시오 그리고 벤치 마크 크기 4000에 대한 타이밍. 그러나 벤치 마크 크기를 5000으로 늘리면 사이클 당 명령이 크게 떨어지고 지연 시간도 점차 늘어나는 것을 알 수 있습니다. 이제 내 질문은 성능 저하가 인 것처럼 보이지만 L1 캐시와 관련이있는 것으로 보입니다. 왜 이렇게 갑자기 발생하는지 설명 할 수 없습니다. 내가 벤치 마크 사이즈 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 |
그래서 제 질문은, 하 스웰을 고려하면 2 * 32 바이트를 제공 할 수 있어야한다 읽고, 32 바이트는 각주기를 저장합니까?
이 코드 gcc를 실현 한
편집은 스마트 제거는이를 방지하려면 0으로 설정되어 있기 때문에 myData.a에 액세스 I했던하는 명시 적으로 설정되는 경우 약간 차이가있는 또 다른 벤치 마크 .
#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;
}
두 번째 예는 하나의 어레이가 읽히고 다른 어레이가 기록됩니다. 그리고 이와 다른 크기를 위해 반환 한 출력에 따라 생성 : 이상 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로 가져 와서 읽음으로 인해 두 번째 액세스 (64 바이트 캐시 라인 32 바이트는 각 반복마다 읽음)에 대해 이 될 것이므로 50 % 이상의 적중률을 보일 것으로 예상됩니다. 그러나 데이터 세트가 L2로 유출되면 L1 히트 율이 2 %로 떨어집니다. 배열이 실제로 L1 캐시 크기와 겹치지 않는다는 것을 고려하면 캐시 충돌로 인한 것이 아닙니다. 그래서이 부분은 여전히 나에게 이해가 가지 않습니다.
+1. 내가 추가 할 수있는 유일한 방법은 내가 본 모든 x86 플랫폼에서 두 배가 8 바이트라는 것입니다. –
실제로 L1에 있지 않은 경우 대역폭을 소비하는 방법과 쓰기 백에 맞습니다. 데이터가 L1에 없으면 처리 장치의 성능을 활용하지 못하는 것은 실망 스럽습니다 (L1보다 큰 스트리밍 유스 케이스의 경우 거의 항상 그렇습니다). – edorado
성능상 중요한 알고리즘은 종종 작은 캐시에 들어갈 수있는 하위 집합으로 작업 집합을 분할합니다 (예 : 캐시 타일링 기법 참조). 기사에 따르면 L2 대역폭은 구형 CPU에 비해 증가했기 때문에 L1 향상에 따라 잡기가 어렵다고 생각합니다. – Leeor