무작위 읽기 병렬화가 잘 작동하는 것 같습니다. 왜 그렇습니까?


18

다음과 같은 매우 간단한 컴퓨터 프로그램을 고려하십시오.

for i = 1 to n:
    y[i] = x[p[i]]

여기서 y 는 바이트의 n 요소 배열이고, p 는 단어 의 n 요소 배열입니다. 여기서, n 은 예를 들어 n = 2 31 이므로, 데이터의 무시할만한 부분 만이 임의의 종류의 캐시 메모리에 적합하다.엑스와이=231

1n 사이에 균일하게 분포 된 난수 로 구성되어 있다고 가정합니다 .1

최신 하드웨어의 관점에서 이것은 다음을 의미해야합니다.

  • 읽는 것이 저렴합니다 (순차적 읽기)[나는]
  • 읽기 는 매우 비쌉니다 (무작위 읽기; 거의 모든 읽기는 캐시 미스입니다. 주 메모리에서 각 개별 바이트를 가져와야합니다)엑스[[나는]]
  • 쓰기 는 저렴합니다 (순차 쓰기).와이[나는]

그리고 이것은 실제로 내가 관찰하고있는 것입니다. 프로그램은 순차적 읽기 및 쓰기 만 수행하는 프로그램과 비교할 때 매우 느립니다. 큰.

이제 질문이 온다 : 이 프로그램 은 현대 멀티 코어 플랫폼에서 얼마나 잘 병렬화 되는가?


내 가설은이 프로그램이 잘 평행하지 않다는 것이었다. 결국 병목 현상이 주요 메모리입니다. 단일 코어는 이미 주 메모리에서 일부 데이터를 기다리는 데 대부분의 시간을 낭비하고 있습니다.

그러나 병목 현상이 이런 종류의 작업 인 알고리즘을 실험하기 시작했을 때 이것은 내가 관찰 한 것이 아닙니다 !

필자는 순진한 for-loop를 OpenMP 병렬 for-loop로 간단히 교체했습니다. 본질적으로 범위 를 더 작은 부품으로 나누고이 부품들을 다른 CPU 코어에서 병렬로 실행합니다.[1,]

저가형 컴퓨터에서는 실제로 속도가 약간 떨어졌습니다. 그러나 고급 플랫폼에서 나는 거의 선형에 가까운 속도 향상을 얻고 있다는 것에 놀랐습니다. 몇 가지 구체적인 예 (정확한 타이밍이 약간 떨어져있을 수 있으며 임의의 변형이 많으며 빠른 실험이었습니다) :

  • 4 코어 Xeon (총 8 코어) 2 개 : 단일 스레드 버전과 비교하여 5-8 속도 향상.

  • 2 개의 6 코어 Xeon (총 12 코어) : 단일 스레드 버전과 비교하여 8-14 속도 향상.

이제 이것은 완전히 예기치 않은 일이었습니다. 질문 :

  1. 정확히 이런 종류의 프로그램이 왜 그렇게 잘 평행 화 되는가? 하드웨어는 어떻게 되나요? (현재 나의 추측은 다음 줄을 따른 것입니다. 다른 스레드에서 임의의 읽기는 "파이프 라인"되어 있으며 이것에 대한 평균 응답 속도는 단일 스레드의 경우보다 훨씬 높습니다.

  2. 그것은이다 다중 스레드 및 멀티 코어를 사용할 필요가 있는 속도 향상을 얻을? 메인 메모리와 CPU 사이의 인터페이스에서 실제로 파이프 라이닝이 발생하면 단일 스레드 응용 프로그램에서 메인 메모리에 , x [ p [ i + 1 ] ] , ... 컴퓨터가 주 메모리에서 관련 캐시 라인을 가져 오기 시작할 수 있습니까? 이것이 원칙적으로 가능하다면 실제로 어떻게 달성합니까?엑스[[나는]]엑스[[나는+1]]

  3. 이런 종류의 프로그램을 분석 하고 성능 을 정확하게 예측하는 데 사용할 수 있는 올바른 이론적 모델 은 무엇입니까 ?


편집 : 이제 여기에 몇 가지 소스 코드와 벤치 마크 결과가 있습니다 : https://github.com/suomela/parallel-random-read

야구장 수치의 일부 예 ( ) :=232

  • 약. 단일 스레드에서 반복 당 42ns (무작위 읽기)
  • 약. 12 개의 코어가있는 반복 당 5ns (무작위 읽기)

답변:


9

이제 메모리 문제를 고려해 봅시다. 고급 Xeon 기반 노드에서 실제로 관찰 한 수퍼 선형 속도 향상은 다음과 같이 정당화됩니다.

병렬 시스템은 메모리가 계층 적이거나 프로그램에서 사용하는 메모리로 액세스 시간이 증가하는 경우 (이산 단계로) 이러한 동작을 보일 수 있습니다. 이 경우 유사한 프로세서를 사용하는 병렬 컴퓨터보다 직렬 프로세서에서 유효 계산 속도가 느려질 수 있습니다. 이는 순차적 알고리즘을 사용하기 때문입니다./

=231

마지막으로 QSM (Queueing Shared Memory) 외에도 공유 메모리 액세스에 대한 경합을 동일한 수준에서 고려하는 다른 이론적 병렬 모델을 알지 못합니다 (OpenMP를 사용할 때 주 메모리가 코어간에 공유됩니다) 캐시는 항상 코어간에 공유됩니다). 어쨌든 모델이 흥미 롭더라도 큰 성공을 거두지 못했습니다.


1
또한 주어진 시간에 프로세스에서 10 x []로드와 같이 다소 고정 된 양의 메모리 레벨 병렬 처리를 제공하는 각 코어로이를 확인할 수 있습니다. 공유 L3에서 0.5 %의 확률로 단일 스레드는 0.995 ** 10 (95 + %)의 확률로 모든로드가 기본 메모리 응답을 기다릴 것을 요구합니다. 총 60 개의 x [] 보류 읽기를 제공하는 6 개의 코어를 사용하면 L3에서 최소 하나의 읽기가 발생할 가능성이 거의 26 %입니다. 또한 MLP가 많을수록 메모리 컨트롤러가 액세스를 더 많이 예약하여 실제 대역폭을 증가시킬 수 있습니다.
Paul A. Clayton

5

__builtin_prefetch ()를 직접 시도하기로 결정했습니다. 다른 사람들이 컴퓨터에서 테스트하고 싶을 경우 답변으로 여기에 게시하고 있습니다. 결과는 Jukka의 설명과 비슷합니다. 요소 20 개를 미리 가져 오는 경우와 요소 0을 미리 가져 오는 경우 실행 시간이 약 20 % 감소합니다.

결과 :

prefetch =   0, time = 1.58000
prefetch =   1, time = 1.47000
prefetch =   2, time = 1.39000
prefetch =   3, time = 1.34000
prefetch =   4, time = 1.31000
prefetch =   5, time = 1.30000
prefetch =   6, time = 1.27000
prefetch =   7, time = 1.28000
prefetch =   8, time = 1.26000
prefetch =   9, time = 1.27000
prefetch =  10, time = 1.27000
prefetch =  11, time = 1.27000
prefetch =  12, time = 1.30000
prefetch =  13, time = 1.29000
prefetch =  14, time = 1.30000
prefetch =  15, time = 1.28000
prefetch =  16, time = 1.24000
prefetch =  17, time = 1.28000
prefetch =  18, time = 1.29000
prefetch =  19, time = 1.25000
prefetch =  20, time = 1.24000
prefetch =  19, time = 1.26000
prefetch =  18, time = 1.27000
prefetch =  17, time = 1.26000
prefetch =  16, time = 1.27000
prefetch =  15, time = 1.28000
prefetch =  14, time = 1.29000
prefetch =  13, time = 1.26000
prefetch =  12, time = 1.28000
prefetch =  11, time = 1.30000
prefetch =  10, time = 1.31000
prefetch =   9, time = 1.27000
prefetch =   8, time = 1.32000
prefetch =   7, time = 1.31000
prefetch =   6, time = 1.30000
prefetch =   5, time = 1.27000
prefetch =   4, time = 1.33000
prefetch =   3, time = 1.38000
prefetch =   2, time = 1.41000
prefetch =   1, time = 1.41000
prefetch =   0, time = 1.59000

암호:

#include <stdlib.h>
#include <time.h>
#include <stdio.h>

void cracker(int *y, int *x, int *p, int n, int pf) {
    int i;
    int saved = pf;  /* let compiler optimize address computations */

    for (i = 0; i < n; i++) {
        __builtin_prefetch(&x[p[i+saved]]);
        y[i] += x[p[i]];
    }
}

int main(void) {
    int n = 50000000;
    int *x, *y, *p, i, pf, k;
    clock_t start, stop;
    double elapsed;

    /* set up arrays */
    x = malloc(sizeof(int)*n);
    y = malloc(sizeof(int)*n);
    p = malloc(sizeof(int)*n);
    for (i = 0; i < n; i++)
        p[i] = rand()%n;

    /* warm-up exercise */
    cracker(y, x, p, n, pf);

    k = 20;
    for (pf = 0; pf < k; pf++) {
        start = clock();
        cracker(y, x, p, n, pf);
        stop = clock();
        elapsed = ((double)(stop-start))/CLOCKS_PER_SEC;
        printf("prefetch = %3d, time = %.5lf\n", pf, elapsed);
    }
    for (pf = k; pf >= 0; pf--) {
        start = clock();
        cracker(y, x, p, n, pf);
        stop = clock();
        elapsed = ((double)(stop-start))/CLOCKS_PER_SEC;
        printf("prefetch = %3d, time = %.5lf\n", pf, elapsed);
    }

    return 0;
}

4
  1. DDR3 액세스는 실제로 파이프 라인입니다. http://www.eng.utah.edu/~cs7810/pres/dram-cs7810-protocolx2.pdf 슬라이드 20과 24는 파이프 라인 읽기 작업 중 메모리 버스에서 발생하는 상황을 보여줍니다.

  2. (부분적으로 잘못되었습니다. 아래 참조) CPU 아키텍처가 캐시 프리 페치를 지원하는 경우 여러 스레드가 필요하지 않습니다. 최신 x86 및 ARM과 다른 많은 아키텍처에는 명시적인 프리 페치 명령어가 있습니다. 또한 많은 사람들이 메모리 액세스에서 패턴을 감지하고 프리 페치를 자동으로 시도합니다. 소프트웨어 지원은 컴파일러에 따라 다릅니다. 예를 들어 GCC 및 Clang에는 명시 적 프리 페칭을위한 __builtin_prefech () 내장 기능이 있습니다.

인텔 스타일의 하이퍼 스레딩은 캐시 미스를 기다리는 데 대부분의 시간을 소비하는 프로그램에서 잘 작동하는 것 같습니다. 필자의 경험에 따르면 계산 집약적 인 워크로드에서 속도 향상은 물리적 코어 수를 약간 초과합니다.

편집 : 포인트 2에서 잘못되었습니다. 프리 페칭은 단일 코어의 메모리 액세스를 최적화 할 수 있지만 여러 코어의 결합 메모리 대역폭은 단일 코어의 대역폭보다 큽니다. 더 큰 것은 CPU에 달려 있습니다.

하드웨어 프리 페처 및 기타 최적화는 벤치마킹을 매우 까다롭게 만듭니다. 명시 적 프리 페칭이 성능에 매우 눈에 띄거나 존재하지 않는 경우를 구성 할 수 있으며,이 벤치 마크는 후자 중 하나입니다.


__builtin_prefech는 매우 유망한 것으로 들립니다. 불행히도, 나의 빠른 실험에서 단일 스레드 성능에 큰 도움이되지 않는 것 같습니다 (<10 %). 이런 종류의 응용 프로그램에서 얼마나 큰 속도 향상을 기대해야합니까?
Jukka Suomela

나는 더 많은 것을 기대했다. 프리 페치가 DSP와 게임에 큰 영향을 미친다는 것을 알고 있으므로 직접 실험해야했습니다. 토끼 구멍이 더 깊어졌습니다.
Juhani Simola

첫 번째 시도는 배열에 저장된 고정 임의 순서를 만든 다음 프리 페치 ( gist.github.com/osimola/7917602 )를 사용하거나 사용하지 않고 순서대로 반복하는 것이 었습니다 . 이는 Core i5에서 약 2 %의 차이를 가져 왔습니다. 프리 페치가 전혀 작동하지 않거나 하드웨어 예측기가 간접 지시를 이해하는 것처럼 들립니다.
Juhani Simola

1
따라서이를 테스트하기 위해 두 번째 시도 ( gist.github.com/osimola/7917568 )는 고정 랜덤 시드에 의해 생성 된 순서대로 메모리에 액세스합니다. 이번에는 프리 페칭 버전이 프리 페칭보다 약 2 배, 프리 페칭보다 1 배 앞당겨졌습니다. 프리 페칭 버전은 프리 페칭이 아닌 버전보다 메모리 액세스 당 더 많은 계산을 수행합니다.
Juhani Simola

이것은 기계에 의존하는 것 같습니다. 나는 Pat Morin의 코드를 아래에서 시도했지만 (평판이 없기 때문에 해당 게시물에 댓글을 달 수 없음) 결과는 다른 프리 페치 값에 대해 1.3 % 이내입니다.
Juhani Simola
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.