2013-10-27 7 views
17

연속 배열에서 스트리밍 성능을 보려면 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 캐시 크기와 겹치지 않는다는 것을 고려하면 캐시 충돌로 인한 것이 아닙니다. 그래서이 부분은 여전히 ​​나에게 이해가 가지 않습니다.

답변

18

내용 요약 :
다른 캐시 수준이 너무 다른 크기의 데이터 세트가 크게 성능에 영향을 미칠 수 있습니다 가진, 같은 기본 작업에 대해 서로 다른 피크 대역폭을 유지할 수 있습니다.

긴 설명 :
그것은 그 스웰, this article에 따라 대한 예를 들어, 고려 매우 놀라운 일이 아니다

2로드 사이클

당 1 개 상점을 유지할 수 있지만, 만 (L1)을 신청했다입니다. 당신이 읽을 경우는 L2

데이터 또는 명령어 캐시 매 사이클마다

하나 부하와 반복 당 하나 명의 저장소가 필요하기 때문에

에 전체 64B 라인을 제공 할 수있는 참조 데이터 세트를 L1에두면 L1 대역폭을 즐기고주기 당 반복 처리량에 도달 할 수 있으며 데이터 세트를 L2로 스필 할 때 더 오래 기다려야합니다. 이것은 시스템에 얼마나 큰 double이 있느냐에 따라 다르지만 결과에 따라 32 비트이므로 4000 * 2 어레이 * 4 바이트 = 32k, 정확하게 L1 크기 및 5000이 초과합니다.

  1. L1-writebacks가 : 기사가 당신이 추가 벌금이다 writebacks을 언급하지 않습니다

    이제 다음 캐시 수준으로 초과하기 시작하면 일이 두 가지 있습니다 대역폭 측면에서 지불해야합니다 (귀하의 퍼포먼스 출력에서 ​​볼 수 있듯이 - 약간 가파르게 보일지라도). 데이터를 L1에 보관하면 L2에서 일부 데이터를 읽는 것은 L2에서 읽은 모든 라인이 L1에서 기존 라인을 버려야한다는 것을 의미하는 반면에 반출을 전혀 할 필요가 없다는 것을 의미합니다. 귀하의 코드 및 명시적인 쓰기 되돌림이 필요합니다. 이러한 트랜잭션은 반복마다 사용하는 두 개의 데이터 요소 값을 읽어야합니다. 저장소의 일부는 사용되지 않아 병합이 필요하기 때문에 이전 데이터를 먼저 읽어야합니다.

  2. 캐시 교체 정책은 - 다음, 최초의 결합 방법을 충전 캐시는 LRU 방식을 사용 가능성이 가장 높은 연관 설정되어 있기 때문에 점에 유의, 당신은 연속적으로 당신의 배열을 통해 이동하기 때문에, 캐시의 사용 패턴은 아마 것 두 번째 방법으로 이동하는 등 - 마지막 길을 채울 때까지는 L2에 필요한 데이터가있는 경우 (큰 데이터 세트의 경우), 모든 행을 첫 번째 방법에서 제거 할 수 있습니다. 그들은 가장 최근에 사용 된 것입니다. 그렇다고해서 다음에 사용하게 될 것임을 의미합니다. 그것은 캐시보다 큰 데이터 세트를 사용하는 LRU의 단점입니다. 단일 방식 (L1 캐시의 1/8)의 최소 크기에 의해 캐시 크기를 초과하면 성능 하락이 때문에이 액세스 패턴, 그래서 갑자기 왜

이 설명합니다.

퍼펙트 결과에 대한 마지막 코멘트가 있습니다. L1 히트 율이 5000 요소의 경우 0으로 떨어질 것이라고 예상했을 것입니다. 그러나 HW 프리 페칭을 사용하면 L1에서 실제 데이터 읽기보다 앞서 실행되는 것처럼 보일 수 있습니다. 대역폭을 측정하고 있기 때문에 데이터를 가져 오는 데 필요한 프리 페치를 기다려야합니다. 실제로드/저장소와 동일한 대역폭을 사용하지만 perf로는 설명 할 수 없으므로 믿을 수 있습니다 당신은 L1 히트를 모두 가지고 있었다. 최소한 내 최선의 추측입니다. 프리 페치를 비활성화하고 다시 측정하여 확인할 수 있습니다 (나는 그러한 조언을 너무 자주 제공하는 것 같습니다. 그러한 끌림에 대해 유감스럽게 생각합니다).


EDIT 1 (다음 당신)

가 두 배 크기에 대한 신비를 해결 제거 된 배열에 대한

큰 캐치 - 그것은 64 비트 참, 그래서 4000 개 요소, 또는 2 개 배열 한 배열 중 각 2000 요소 (귀하의 수정 후)의 각 L1에 맞게 수 있습니다. 이제 유출은 3000 개의 요소에서 발생합니다. L1이 2 개의 별개의 스트림보다 앞서 실행되도록 충분한 프리 페치를 발행 할 수 없으므로 L1 적중률이 낮습니다.

각로드가 2 번의 반복을 위해 64 바이트 라인을 가져올 것으로 예상되는 것처럼 - 나는 꽤 흥미로운 것을보고있다. 메모리 유닛에서 나온로드의 수 (L1 히트 + L1 미스)를 합하면, 2000 요소의 경우는 1000 요소에서 거의 2 배이지만 3000 및 4000의 경우는 각각 3 배 및 4 배가 아니라 절반입니다. 특히, 배열 당 3000 개 요소를 사용하면 2000 개 요소보다 액세스가 적습니다!
이것은 메모리 유닛이 각각의 2 개의로드를 하나의 메모리 액세스로 병합 할 수 있다고 의심하게 만듭니다. 단 L2 이상으로 갈 때만 가능합니다. 그 점을 생각할 때 의미가 있습니다. 이미 L2에 대한 보류 상태 인 경우 L2를 검색 할 다른 액세스 권한을 발급 할 필요가 없으며 해당 수준에서 더 낮은 대역폭을 완화 할 수있는 가능한 방법입니다. 어떤 이유로 두 번째로드가 L1 조회로 간주되지 않고보고 싶은 조회 율에 도움이되지 않는다고 추측합니다 (얼마나 많은로드가 실행을 통과하는지 나타내는 카운터를 확인할 수 있습니다. 아마도 사실 일 것이다). 이것은 단지 직감이지만, 카운터가 어떻게 정의되어 있는지는 잘 모르겠지만, 우리가 보는 액세스 수와 일치합니다.

+1

+1. 내가 추가 할 수있는 유일한 방법은 내가 본 모든 x86 플랫폼에서 두 배가 8 바이트라는 것입니다. –

+0

실제로 L1에 있지 않은 경우 대역폭을 소비하는 방법과 쓰기 백에 맞습니다. 데이터가 L1에 없으면 처리 장치의 성능을 활용하지 못하는 것은 실망 스럽습니다 (L1보다 큰 스트리밍 유스 케이스의 경우 거의 항상 그렇습니다). – edorado

+1

성능상 중요한 알고리즘은 종종 작은 캐시에 들어갈 수있는 하위 집합으로 작업 집합을 분할합니다 (예 : 캐시 타일링 기법 참조). 기사에 따르면 L2 대역폭은 구형 CPU에 비해 ​​증가했기 때문에 L1 향상에 따라 잡기가 어렵다고 생각합니다. – Leeor