"캐시 친화적"코드 란 무엇입니까?


738

" 캐시 비친 화 코드 "와 " 캐시 친화 "코드 의 차이점은 무엇입니까 ?

캐시 효율적인 코드를 작성하려면 어떻게해야합니까?


28
이것은 힌트를 줄 것입니다 : stackoverflow.com/questions/9936132/…
Robert Martin

4
캐시 라인의 크기도 알고 있어야합니다. 최신 프로세서에서는 종종 64 바이트입니다.
John Dibling

3
여기 또 다른 좋은 기사가 있습니다. 이 원칙은 모든 OS (Linux, MaxOS 또는 Windows)에서 C / C ++ 프로그램에 적용됩니다. lwn.net/Articles/255364
paulsm4


답변:


965

예비

최신 컴퓨터에서는 가장 낮은 수준의 메모리 구조 ( 레지스터 ) 만 단일 클록 주기로 데이터를 이동할 수 있습니다. 그러나 레지스터는 매우 비싸고 대부분의 컴퓨터 코어에는 수십 개 미만의 레지스터가 있습니다 ( 총 수백에서 수천 바이트 ). 메모리 스펙트럼 ( DRAM ) 의 다른 쪽 끝 에서, 메모리는 매우 저렴하지만 (즉, 문자 그대로 수백만 배 저렴 ) 데이터 수신 요청 후 수백 사이클이 걸린다. 캐시 메모리 는 초고가와 초고가 사이의 간격을 메우기 위해 사용 됩니다속도와 비용이 감소하면서 L1, L2, L3으로 명명되었습니다. 아이디어는 대부분의 실행 코드가 종종 작은 변수 세트에 충돌하고 나머지는 훨씬 더 큰 변수 세트에 자주 충돌한다는 것입니다. 프로세서가 L1 캐시에서 데이터를 찾을 수 없으면 L2 캐시를 찾습니다. 없는 경우 L3 캐시 및없는 경우 주 메모리. 이러한 각각의 "누락"은 시간이 많이 걸립니다.

(시스템 메모리가 너무 하드 디스크 스토리지이므로 캐시 메모리는 시스템 메모리와 유사합니다. 하드 디스크 스토리지는 매우 저렴하지만 매우 느립니다).

캐싱은 대기 시간 의 영향을 줄이는 주요 방법 중 하나입니다 . Herb Sutter의 말을 인용하려면 (아래 링크 참조) : 대역폭을 늘리는 것은 쉽지만 대기 시간을 벗어나는 길을 살 수는 없습니다 .

데이터는 항상 메모리 계층 구조를 통해 검색됩니다 (가장 작은 == 가장 빠름에서 가장 느림). 캐시 적중 / 미스는 일반적으로 CPU의 캐시의 최고 수준의 히트 / 미스를 말한다 - 최고 수준으로 내가 가장 큰 == 느린 것을 의미한다. 캐시 적중률은 모든 캐시 누락으로 인해 많은 시간 (RAM의 경우 수백 사이클, HDD의 경우 수천만 사이클 )이 걸리는 RAM에서 데이터를 가져 오거나 (또는 ​​더 나쁘게) 성능에 중요합니다 . 이에 비해 (가장 높은 수준의) 캐시에서 데이터를 읽는 것은 일반적으로 몇 번의 주기만 걸립니다.

최신 컴퓨터 아키텍처에서 성능 병목 현상은 CPU 다이를 떠나고 있습니다 (예 : RAM에 액세스). 시간이지나면서 악화 될뿐입니다. 프로세서 주파수의 증가는 현재 더 이상 성능 향상과 관련이 없습니다. 문제는 메모리 액세스입니다. 따라서 CPU의 하드웨어 설계 노력은 현재 캐시 최적화, 프리 페치, 파이프 라인 및 동시성에 중점을두고 있습니다. 예를 들어, 최신 CPU는 캐시에 약 85 %의 다이를 사용하고 데이터를 저장 / 이동하는 데 최대 99 %를 소비합니다!

이 주제에 대해 언급해야 할 것이 많이 있습니다. 다음은 캐시, 메모리 계층 및 적절한 프로그래밍에 대한 훌륭한 참고 자료입니다.

캐시 친화적 인 코드의 주요 개념

캐시 친화적 인 코드의 매우 중요한 측면은 로컬 성의 원칙에 관한 것이며 , 효율적인 캐싱을 위해 관련 데이터를 메모리에 가까이 배치하는 것이 목표입니다. CPU 캐시와 관련하여 캐시 라인을 알고 있어야 작동 방식을 이해할 수 있습니다. 캐시 라인은 어떻게 작동합니까?

캐싱을 최적화하려면 다음과 같은 특정 측면이 매우 중요합니다.

  1. 시간적 지역성 : 주어진 메모리 위치에 액세스했을 때 가까운 시일 내에 같은 위치에 다시 액세스 할 가능성이 있습니다. 이상적으로이 정보는 여전히 그 시점에 캐시됩니다.
  2. 공간적 위치 : 관련 데이터를 서로 가깝게 배치하는 것을 말합니다. 캐싱은 CPU뿐만 아니라 여러 수준에서 발생합니다. 예를 들어, RAM에서 읽을 때 일반적으로 프로그램이 해당 데이터를 곧 요구하기 때문에 특별히 요청한 것보다 더 큰 메모리 청크를 가져옵니다. HDD 캐시는 동일한 사고 방식을 따릅니다. 특히 CPU 캐시의 경우 캐시 라인 의 개념 이 중요합니다.

적절한 사용 컨테이너

캐시 친화적이고 캐시 비 친화적 인 간단한 예는 다음과 같습니다. std::vectorstd::list. 의 요소는 std::vector연속 메모리에 저장되며, 액세스하는 방식은의 요소를 액세스하는 보다 캐시에 훨씬 친숙 std::list합니다. 이것은 공간적 지역성 때문입니다.

이 유튜브 클립 에서 Bjarne Stroustrup이 매우 멋진 그림을 제공 합니다 (링크에 @Mohammad Ali Baydoun에게 감사드립니다!).

데이터 구조 및 알고리즘 설계에서 캐시를 무시하지 마십시오

가능하면 캐시를 최대한 활용할 수있는 방식으로 데이터 구조와 계산 순서를 조정하십시오. 이와 관련하여 일반적인 기술은 캐시 차단 (Archive.org 버전) 인데, 이는 고성능 컴퓨팅 (예 : ATLAS 참조 ) 에서 매우 중요 합니다.

암시적인 데이터 구조 이해 및 활용

현장의 많은 사람들이 때때로 잊어 버린 또 다른 간단한 예는 열 주요입니다 (예 : ,) 대 행 순서 순서 (예 : ,2 차원 배열을 저장합니다. 예를 들어 다음 매트릭스를 고려하십시오.

1 2
3 4

행 메이저 순서에서 이것은 메모리에 1 2 3 4; 열 주요 순서에서 이것은로 저장됩니다 1 3 2 4. 이 순서를 악용하지 않는 구현은 캐시 문제에 빠르게 발생한다는 것을 쉽게 알 수 있습니다. 불행히도, 나는 내 도메인 (기계 학습)에서 이와 같은 것들을 매우 자주 본다 . @MatteoItalia는이 예제를 그의 답변에서 더 자세히 보여주었습니다.

메모리에서 행렬의 특정 요소를 가져올 때 그 근처의 요소도 가져 와서 캐시 라인에 저장됩니다. 순서가 악용되면 메모리 액세스 횟수가 줄어 듭니다 (다음 계산에 필요한 다음 몇 개의 값이 이미 캐시 라인에 있기 때문에).

간단히하기 위해, 캐시는 2 개의 매트릭스 요소를 포함 할 수있는 단일 캐시 라인을 포함하고 주어진 요소가 메모리에서 페치 될 때 다음 요소도 있다고 가정합니다. 위의 예제 2x2 행렬의 모든 요소에 대해 합계를 가져오고 싶다고 가정 해 봅시다 M.

순서 활용 (예 : 열 색인을 먼저 변경) ) :

M[0][0] (memory) + M[0][1] (cached) + M[1][0] (memory) + M[1][1] (cached)
= 1 + 2 + 3 + 4
--> 2 cache hits, 2 memory accesses

순서를 악용하지 않음 (예 : 행 색인을 먼저 변경) ) :

M[0][0] (memory) + M[1][0] (memory) + M[0][1] (memory) + M[1][1] (memory)
= 1 + 3 + 2 + 4
--> 0 cache hits, 4 memory accesses

이 간단한 예에서, 순서를 이용하면 실행 속도가 약 두 배가됩니다 (메모리 액세스는 합계 계산보다 훨씬 많은주기를 필요로하기 때문에). 실제로 성능 차이는 훨씬 클 수 있습니다 .

예측할 수없는 가지를 피하십시오

최신 아키텍처는 파이프 라인을 특징으로하며 컴파일러는 메모리 액세스로 인한 지연을 최소화하기 위해 코드를 재정렬하는 데 매우 능숙 해지고 있습니다. 중요 코드에 예측할 수없는 분기가 포함 된 경우 데이터를 프리 페치하기가 어렵거나 불가능합니다. 이것은 간접적으로 더 많은 캐시 누락을 초래할 것입니다.

이 설명 아주 잘 여기 (링크에 대한 @ 0x90 덕분에) : 왜 빠른 정렬되지 않은 배열을 처리하는 것보다 정렬 된 배열을 처리?

가상 기능을 피하십시오

문맥 상에 , virtual방법은 캐시 미스와 관련하여 논란의 여지가있는 문제를 나타냅니다 (성능 측면에서 가능한 경우 피해야한다는 일반적인 합의가 존재합니다). 가상 함수는 룩업 동안 캐시 미스를 유도 할 수 있지만,이 경우에만 발생하는 경우 이 일부에 의해 비 문제로 간주하므로, 특정 기능이 자주 호출되지 않습니다 (그렇지 않으면 가능성이 캐시 될 것이다). 이 문제에 대한 참조 는 C ++ 클래스에서 가상 메소드를 사용하는 데 드는 성능 비용은 얼마입니까?를 확인하십시오.

일반적인 문제

다중 프로세서 캐시를 사용하는 최신 아키텍처에서 흔히 발생하는 문제를 허위 공유 라고 합니다 . 이것은 각각의 개별 프로세서가 다른 메모리 영역에서 데이터를 사용하려고 시도하고 동일한 캐시 라인에 데이터를 저장하려고 할 때 발생합니다 . 이로 인해 다른 프로세서가 사용할 수있는 데이터가 들어있는 캐시 라인이 반복해서 덮어 쓰기됩니다. 효과적으로 다른 스레드는이 상황에서 캐시 누락을 유도하여 서로를 대기시킵니다. (링크에 @Matt 덕분에) : 캐시 라인 크기에 맞추는 방법과시기?

RAM 메모리에서 캐싱이 제대로 이루어지지 않는 경우 (이 문맥에서는 의미가 없을 수도 있음)를 스 래싱 이라고 합니다. 프로세스가 디스크 액세스가 필요한 페이지 폴트 (예 : 현재 페이지에없는 메모리에 액세스)를 지속적으로 생성 할 때 발생합니다.


27
아마 당신은 또한 멀티 스레드 코드에서 데이터가 너무 로컬 (예 : 허위 공유)
TemplateRex

2
칩 설계자가 유용하다고 생각하는 수준의 캐시가있을 수 있습니다. 일반적으로 속도와 크기의 균형을 유지합니다. L1 캐시를 L5만큼 크고 빠르게 만들 수 있다면 L1 만 있으면됩니다.
라파엘 밥티스타

24
나는 빈 의견 동의가 StackOverflow에서 비 승인되었다는 것을 알고 있지만, 지금까지 내가 본 것 중 가장 명확하고 가장 좋은 대답입니다. 잘 했어, 마크
Jack Aidley

2
@JackAidley 감사합니다! 이 질문에 대한 관심을 보았을 때 많은 사람들이 다소 광범위한 설명에 관심이있을 것으로 생각했습니다. 도움이 되서 다행입니다.
Marc Claesen

1
언급하지 않은 것은 캐시 친화적 인 데이터 구조가 캐시 라인에 적합하고 캐시 라인을 최적으로 사용하도록 메모리에 맞춰 지도록 설계되었다는 것입니다. 그래도 좋은 답변입니다! 대박.
Matt

140

@Marc Claesen의 대답 외에도 캐시 친화적이지 않은 코드의 유익한 고전적인 예는 행 단위가 아닌 열 단위로 C 2 차원 배열 (예 : 비트 맵 이미지)을 스캔하는 코드라고 생각합니다.

행에 인접한 요소들도 메모리에 인접하므로, 순차적으로 액세스하는 것은 오름차순 메모리 순서로 액세스하는 것을 의미하고; 캐시는 인접한 메모리 블록을 프리 페치하는 경향이 있기 때문에 캐시 친화적입니다.

대신 같은 열의 요소가 서로 메모리에 떨어져 있기 때문에 (특히, 거리가 행의 크기와 같기 때문에) 열 단위로 이러한 요소에 액세스하는 것은 캐시 친화적이지 않으므로이 액세스 패턴을 사용하면 메모리에서 뛰어 다니면서 메모리에있는 요소를 검색하는 캐시의 노력을 낭비 할 수 있습니다.

그리고 성능을 망치기 위해 필요한 것은

// Cache-friendly version - processes pixels which are adjacent in memory
for(unsigned int y=0; y<height; ++y)
{
    for(unsigned int x=0; x<width; ++x)
    {
        ... image[y][x] ...
    }
}

// Cache-unfriendly version - jumps around in memory for no good reason
for(unsigned int x=0; x<width; ++x)
{
    for(unsigned int y=0; y<height; ++y)
    {
        ... image[y][x] ...
    }
}

이 효과는 캐시가 작은 시스템 및 / 또는 큰 어레이를 사용하는 시스템 (예 : 현재 머신에서 10+ 메가 픽셀 24bpp 이미지)에서 상당히 극적 일 수 있습니다 (수십 배속). 이러한 이유로 많은 수직 스캔을 수행해야하는 경우 이미지를 먼저 90도 회전하고 나중에 다양한 분석을 수행하여 캐시에 친숙하지 않은 코드를 회전으로 제한하는 것이 좋습니다.


오류, x <width 여야합니까?
mowwwalker

13
최신 이미지 편집기는 타일을 내부 스토리지로 사용합니다 (예 : 64x64 픽셀 블록). 주변 픽셀이 대부분 양방향으로 메모리에 있기 때문에 로컬 작업 (손 잡기 배치, 흐림 필터 실행)에 훨씬 캐시 친화적입니다.
maxy

내 컴퓨터에서 비슷한 예제의 타이밍을 시도했는데 시간이 동일하다는 것을 알았습니다. 다른 사람이 타이밍을 시도 했습니까?
gsingh2011

@ I3arnon : nope, 첫 번째는 캐시 친화적입니다. 일반적으로 C 배열에는 행 주요 순서 로 저장됩니다 (물론 어떤 이유로 이미지가 열 주요 순서로 저장되는 경우에는 반대입니다).
Matteo Italia

1
@Gauthier : 예, 첫 번째 스 니펫이 좋습니다. 필자가이 글을 썼을 때, "[응용 프로그램의 성능을 망치는 데 필요한 모든 것]은 ...에서 ..."으로
Matteo Italia

88

캐시 사용 최적화에는 크게 두 가지 요소가 있습니다.

참조의 지역

다른 사람들이 이미 언급 한 첫 번째 요소는 참조의 지역성입니다. 참조의 지역 성은 실제로 공간과 시간의 두 가지 차원을 가지고 있습니다.

  • 공간

공간 차원도 두 가지로 나뉩니다. 첫째, 정보를 조밀하게 압축하여 더 많은 정보가 제한된 메모리에 적합합니다. 이는 예를 들어 포인터로 연결된 작은 노드를 기반으로 데이터 구조를 정당화하기 위해 계산 복잡성을 대폭 개선해야 함을 의미합니다.

둘째, 함께 처리 될 정보도 함께 원합니다. 일반적인 캐시는 "라인"으로 작동합니다. 즉, 일부 정보에 액세스하면 근처 주소의 다른 정보가 터치 한 부분으로 캐시에로드됩니다. 예를 들어, 1 바이트를 터치하면 캐시가 해당 바이트 근처에 128 바이트 또는 256 바이트를로드 할 수 있습니다. 이를 활용하기 위해 일반적으로 동시에로드 된 다른 데이터를 사용할 가능성을 최대화하도록 데이터를 정렬하려고합니다.

아주 간단한 예를 들어, 선형 검색이 예상보다 이진 검색보다 훨씬 경쟁력이 있음을 의미 할 수 있습니다. 캐시 라인에서 하나의 항목을로드하면 해당 캐시 라인의 나머지 데이터를 사용하는 것은 거의 무료입니다. 이진 검색은 액세스하는 캐시 라인 수를 줄일 수있을 정도로 데이터가 클 때만 이진 검색이 훨씬 빨라집니다.

  • 시각

시간 차원은 일부 데이터에 대해 일부 작업을 수행 할 때 해당 데이터에 대한 모든 작업을 한 번에 수행하기를 원한다는 것을 의미합니다.

이 태그를 C ++로 태그 했으므로 비교적 캐시에 친숙하지 않은 디자인의 고전적인 예를 살펴 보겠습니다 std::valarray. valarray과부하 대부분의 산술 연산자는, 그래서 (예를 들어) 말할 수있다 a = b + c + d;(여기서 a, b, cd모든 valarrays 있습니다) 그 배열의 요소 현명한 추가 할 수 있습니다.

이것의 문제는 한 쌍의 입력을 통해 걷고, 일시적으로 결과를 만들고, 다른 쌍의 입력을 통해 걷고 있다는 것입니다. 많은 양의 데이터를 사용하면 다음 계산에 사용하기 전에 한 계산의 결과가 캐시에서 사라질 수 있으므로 최종 결과를 얻기 전에 데이터를 반복적으로 읽고 (쓰기)합니다. 최종 결과의 각 요소가 같을 경우 (a[n] + b[n]) * (c[n] + d[n]);, 우리는 일반적으로 각을 읽을 선호하는 것 a[n], b[n], c[n]그리고 d[n]한 번, 계산을 수행 한 결과, 증가 쓰기 n우리가 완료 때까지 반복 '. 2

라인 공유

두 번째 주요 요소는 회선 공유를 피하는 것입니다. 이를 이해하려면 캐시를 구성하는 방법을 백업하고 약간 살펴 봐야합니다. 가장 간단한 형태의 캐시는 직접 매핑됩니다. 이는 주 메모리의 하나의 주소 만 캐시의 하나의 특정 지점에만 저장할 수 있음을 의미합니다. 캐시의 동일한 지점에 매핑되는 두 개의 데이터 항목을 사용하는 경우 데이터가 잘못 작동합니다. 한 데이터 항목을 사용할 때마다 다른 데이터를 캐시에서 비워서 다른 데이터를위한 공간을 확보해야합니다. 캐시의 나머지 부분은 비어있을 수 있지만 해당 항목은 캐시의 다른 부분을 사용하지 않습니다.

이를 방지하기 위해 대부분의 캐시를 "set associative"라고합니다. 예를 들어, 4-way set-associative 캐시에서 주 메모리의 모든 항목을 캐시의 4 가지 다른 위치에 저장할 수 있습니다. 따라서 캐시가 항목을로드 할 때 4 개 중 가장 최근에 사용한 3 개의 항목을 찾아 주 메모리로 플러시하고 대신 새 항목을로드합니다.

직접 매핑 된 캐시의 경우 동일한 캐시 위치에 매핑되는 두 피연산자가 잘못된 동작을 일으킬 수 있습니다. N-way 세트 연관 캐시는 숫자를 2에서 N + 1로 증가시킵니다. 캐시를 더 많은 "방법"으로 구성하면 추가 회로가 필요하고 일반적으로 느리게 실행되므로 (예를 들어) 8192-way 세트 연관 캐시도 좋은 솔루션이 아닙니다.

궁극적 으로이 요소는 휴대용 코드에서 제어하기가 더 어렵습니다. 데이터의 위치에 대한 제어는 일반적으로 상당히 제한적입니다. 더군다나, 주소에서 캐시로의 정확한 매핑은 유사한 프로세서마다 다릅니다. 그러나 경우에 따라 큰 버퍼를 할당 한 다음 동일한 캐시 라인을 공유하는 데이터에 대비하기 위해 할당 한 것의 일부만 사용하는 등의 작업을 수행하는 것이 좋습니다. 이에 따라 행동하십시오).

  • 허위 공유

"false sharing"이라는 또 다른 관련 항목이 있습니다. 이는 두 개 이상의 프로세서 / 코어가 별도의 데이터를 가지고 있지만 동일한 캐시 라인에있는 멀티 프로세서 또는 멀티 코어 시스템에서 발생합니다. 이렇게하면 두 개의 프로세서 / 코어가 각각 별도의 데이터 항목이 있더라도 데이터에 대한 액세스를 조정합니다. 특히 두 개가 교대로 데이터를 수정하는 경우 데이터가 프로세서간에 지속적으로 셔틀되어야하므로 막대한 속도 저하가 발생할 수 있습니다. 캐시를보다 "방법"또는 이와 유사한 방식으로 구성하여 쉽게 치료할 수는 없습니다. 이를 방지하는 기본 방법은 두 개의 스레드가 동일한 캐시 라인에있을 수있는 데이터를 거의 수정하지 않는 것이 바람직합니다 (데이터가 할당되는 주소를 제어하는 ​​데 어려움이 있다는 동일한 경고가 있음).


  1. C ++을 잘 아는 사람들은 이것이 표현식 템플릿과 같은 것을 통해 최적화가 가능한지 궁금 할 것입니다. 나는 대답이 그렇다는 것을 확신합니다. 그렇다면 가능할 것입니다. 그렇다면 상당히 상당한 승리 일 것입니다. 그러나 아무도 그렇게하지 않았다는 것을 알지 못하고 조금만 valarray익숙해지면 적어도 어느 누구도 그렇게하는 것을보고 약간 놀랐습니다.

  2. 누군가가 valarray(성능을 위해 특별히 설계된) 이것이 어떻게 잘못 될 수 있는지 궁금해하는 경우 한 가지로 귀착됩니다. 이것은 실제로 오래된 주 크레이와 같은 기계를 위해 설계되었으며 빠른 주 메모리를 사용하고 캐시는 사용하지 않았습니다. 그들에게는 이것이 거의 이상적인 디자인이었습니다.

  3. 그렇습니다. 단순화하고 있습니다. 대부분의 캐시는 실제로 가장 최근에 사용한 항목을 정확하게 측정하지 않지만 각 액세스에 대해 전체 타임 스탬프를 유지하지 않고도 그와 가까운 휴리스틱을 사용합니다.


1
나는 당신의 대답에있는 여분의 정보, 특히 valarray예를 좋아합니다 .
Marc Claesen

1
+1 마침내 : 세트 연관성에 대한 명확한 설명! 추가 편집 : 이것은 SO에 대한 가장 유익한 답변 중 하나입니다. 감사합니다.
엔지니어

32

데이터 지향 디자인의 세계에 오신 것을 환영합니다. 기본적인 진언은 virtual더 나은 지역성을 향한 모든 단계 인 분류, 분기 제거, 배치, 통화 제거 입니다.

C ++로 질문에 태그를 지정 했으므로 필수 C ++ Bullshit이 있습니다. Tony Albrecht의 객체 지향 프로그래밍함정 또한이 주제에 대한 훌륭한 소개입니다.


1
배치 란 무엇을 의미합니까, 이해하지 못할 수도 있습니다.
0x90

5
배치 : 단일 객체에서 작업 단위를 수행하는 대신 배치 개체에서 수행합니다.
arul

AKA 차단, 레지스터 차단, 캐시 차단.
0x90

1
차단 / 비 차단은 일반적으로 동시 환경에서 객체가 작동하는 방식을 나타냅니다.
arul

2
배치 == 벡터화
Amro

23

캐시 비친 화 대 캐시 친화적 코드의 전형적인 예는 매트릭스 곱셈의 "캐시 차단"입니다.

순진 행렬 곱셈은 다음과 같습니다.

for(i=0;i<N;i++) {
   for(j=0;j<N;j++) {
      dest[i][j] = 0;
      for( k==;k<N;i++) {
         dest[i][j] += src1[i][k] * src2[k][j];
      }
   }
}

경우 N경우, 예를 큰 N * sizeof(elemType)캐시 크기보다 큰 경우, 모든 단일 액세스하는 src2[k][j]캐시 미스 될 것입니다.

이를 위해 캐시를 최적화하는 방법에는 여러 가지가 있습니다. 내부 루프에서 캐시 라인 당 하나의 항목을 읽는 대신 모든 항목을 사용하십시오.

int itemsPerCacheLine = CacheLineSize / sizeof(elemType);

for(i=0;i<N;i++) {
   for(j=0;j<N;j += itemsPerCacheLine ) {
      for(jj=0;jj<itemsPerCacheLine; jj+) {
         dest[i][j+jj] = 0;
      }
      for( k=0;k<N;k++) {
         for(jj=0;jj<itemsPerCacheLine; jj+) {
            dest[i][j+jj] += src1[i][k] * src2[k][j+jj];
         }
      }
   }
}

캐시 라인 크기가 64 바이트이고 32 비트 (4 바이트) 플로트에서 작동하는 경우 캐시 라인 당 16 개의 항목이 있습니다. 이 간단한 변환을 통한 캐시 누락 수는 약 16 배 줄어 듭니다.

환상적인 변형은 2D 타일에서 작동하고 여러 캐시 (L1, L2, TLB) 등을 최적화합니다.

인터넷 검색 "캐시 차단"의 일부 결과 :

http://stumptown.cc.gt.atl.ga.us/cse6230-hpcta-fa11/slides/11a-matmul-goto.pdf

http://software.intel.com/en-us/articles/cache-blocking-techniques

최적화 된 캐시 차단 알고리즘의 멋진 비디오 애니메이션.

http://www.youtube.com/watch?v=IFWgwGMMrh0

루프 타일링은 매우 밀접한 관련이 있습니다.

http://en.wikipedia.org/wiki/Loop_tiling


7
기사 를 읽는 사람들은 두 개의 2000x2000 행렬을 곱하여 "캐시 친화적"ikj- 알고리즘과 비우호적 ijk- 알고리즘을 테스트 한 행렬 곱셈에 대한 기사에 관심이있을 수 있습니다 .
마틴 토마

3
k==;나는 이것이 오타되기를 바라고있다?
TrebledJ

13

오늘날 프로세서는 여러 수준의 계단식 메모리 영역에서 작동합니다. 따라서 CPU에는 CPU 칩 자체에 많은 메모리가 있습니다. 이 메모리에 매우 빠르게 액세스 할 수 있습니다. CPU에 있지 않고 상대적으로 액세스 속도가 느린 시스템 메모리에 도달 할 때까지 각각의 액세스 속도가 느리고 캐시보다 다른 캐시 레벨이 있습니다.

논리적으로 CPU의 명령어 세트에서는 거대한 가상 주소 공간의 메모리 주소 만 참조하면됩니다. 단일 메모리 주소에 액세스하면 CPU가 가져옵니다. 예전에는 단일 주소 만 가져 왔습니다. 그러나 오늘날 CPU는 요청한 비트 주위에 많은 메모리를 가져 와서 캐시에 복사합니다. 특정 주소를 요청하면 가까운 시일 내에 주소를 요청할 가능성이 높다고 가정합니다. 예를 들어 버퍼를 복사하는 경우 연속 주소를 읽고 쓸 수 있습니다.

따라서 오늘 주소를 가져올 때 첫 번째 레벨의 캐시를 검사하여 이미 해당 주소를 캐시로 읽었는지 확인합니다.이를 찾지 못하면 캐시 미스이므로 다음 레벨로 이동해야합니다. 캐시를 찾아 메인 메모리로 나가야 할 때까지 캐시를 찾습니다.

캐시 친화적 인 코드는 액세스를 메모리에서 서로 가깝게 유지하여 캐시 누락을 최소화합니다.

예를 들어, 거대한 2 차원 테이블을 복사하려고한다고 상상해보십시오. 메모리에서 연속으로 도달 행으로 구성되며 한 행은 바로 다음 행을 따릅니다.

왼쪽에서 오른쪽으로 한 번에 한 행씩 요소를 복사 한 경우 캐시 친화적입니다. 한 번에 한 열씩 테이블을 복사하기로 결정한 경우 정확히 같은 양의 메모리를 복사하지만 캐시가 비우호적입니다.


4

데이터는 캐시 친화적이어야 할뿐만 아니라 코드에도 중요하다는 점을 분명히해야합니다. 이것은 분기 예측, 명령 순서 변경, 실제 구분 및 기타 기술을 피하는 것입니다.

일반적으로 코드가 밀도가 높을수록 코드 저장에 필요한 캐시 라인이 줄어 듭니다. 이로 인해 더 많은 캐시 라인이 데이터에 사용 가능해집니다.

코드는 일반적으로 자체 캐시 라인을 하나 이상 필요로하므로 데이터를위한 캐시 라인이 줄어듦에 따라 모든 곳에서 함수를 호출해서는 안됩니다.

함수는 캐시 라인 정렬 친화적 인 주소에서 시작해야합니다. 이를 위해 (gcc) 컴파일러 스위치가 있지만 기능이 매우 짧은 경우 각 캐시 라인을 점유하는 것은 낭비가 될 수 있습니다. 예를 들어, 가장 자주 사용되는 함수 중 3 개가 하나의 64 바이트 캐시 라인에 들어가는 경우 각 라인에 자체 라인이있는 경우보다 낭비가 적고 다른 용도로는 2 개의 캐시 라인을 사용할 수 없게됩니다. 일반적인 정렬 값은 32 또는 16 일 수 있습니다.

따라서 코드를 조밀하게 만들기 위해 약간의 시간을 투자하십시오. 다른 구문을 테스트하고 생성 된 코드 크기와 프로파일을 컴파일하고 검토하십시오.


2

@Marc Claesen이 언급 한 것처럼 캐시 친화적 인 코드를 작성하는 방법 중 하나는 데이터가 저장되는 구조를 이용하는 것입니다. 캐시 친화적 인 코드를 작성하는 또 다른 방법은 다음과 같습니다. 데이터 저장 방식 변경; 그런 다음이 새 구조에 저장된 데이터에 액세스하기 위해 새 코드를 작성하십시오.

이는 데이터베이스 시스템이 테이블의 튜플을 선형화하고 저장하는 방법에 적합합니다. 테이블의 튜플 (예 : 행 저장소 및 열 저장소)을 저장하는 두 가지 기본 방법이 있습니다. 이름에서 알 수 있듯이 행 저장에서 튜플은 행 단위로 저장됩니다. 라는 이름의 테이블 가정하자 Product저장되는 것은 3 속성, 즉이 int32_t key, char name[56]int32_t price튜플의 전체 크기는 그래서, 64바이트.

Product크기가 N 인 구조체 의 배열을 만들어 주 메모리에서 매우 기본적인 행 저장소 쿼리 실행을 시뮬레이션 할 수 있습니다 . 여기서 N은 테이블의 행 수입니다. 이러한 메모리 레이아웃을 구조체 배열이라고도합니다. 따라서 Product의 구조체는 다음과 같습니다.

struct Product
{
   int32_t key;
   char name[56];
   int32_t price'
}

/* create an array of structs */
Product* table = new Product[N];
/* now load this array of structs, from a file etc. */

마찬가지로 Product테이블의 각 속성에 대해 하나의 배열 인 크기 N의 3 개의 배열을 만들어 주 메모리에서 매우 기본적인 열 저장소 쿼리 실행을 시뮬레이션 할 수 있습니다 . 이러한 메모리 레이아웃을 배열의 구조체라고도합니다. 따라서 Product의 각 속성에 대한 3 개의 배열은 다음과 같습니다.

/* create separate arrays for each attribute */
int32_t* key = new int32_t[N];
char* name = new char[56*N];
int32_t* price = new int32_t[N];
/* now load these arrays, from a file etc. */

이제 구조체 배열 (행 레이아웃)과 3 개의 개별 배열 (열 레이아웃)을 모두로드 한 후 Product메모리 에있는 테이블에 행 저장소와 열 저장소가 있습니다.

이제 캐시 친화적 인 코드 부분으로 넘어갑니다. 테이블의 워크로드가 price 속성에 대한 집계 쿼리를 갖도록되어 있다고 가정하십시오. 와 같은

SELECT SUM(price)
FROM PRODUCT

행 저장소의 경우 위의 SQL 쿼리를

int sum = 0;
for (int i=0; i<N; i++)
   sum = sum + table[i].price;

열 저장소의 경우 위의 SQL 쿼리를

int sum = 0;
for (int i=0; i<N; i++)
   sum = sum + price[i];

열 저장소의 코드는 속성의 하위 집합 만 필요하고 열 레이아웃에서는 즉, 가격 열에 만 액세스하기 때문에이 쿼리의 행 레이아웃 코드보다 빠릅니다.

캐시 라인 크기가 64바이트 라고 가정하십시오 .

캐시 라인을 읽을 때 행 레이아웃의 경우 cacheline_size/product_struct_size = 64/64 = 1구조체 크기가 64 바이트이고 전체 캐시 라인을 채우므로 모든 튜플에 대해 캐시 누락이 발생하기 때문에 1 ( ) 튜플 의 가격 값만 읽습니다. 행 레이아웃.

캐시 라인을 읽을 때 열 레이아웃의 경우 16 ( cacheline_size/price_int_size = 64/4 = 16) 튜플 의 가격 값을 읽습니다. 메모리에 저장된 16 개의 인접한 가격 값이 캐시로 가져 오기 때문에 16 번째 튜플마다 캐시 미스가 발생하기 때문에 열 레이아웃.

따라서 주어진 쿼리의 경우 열 레이아웃이 빨라지고 테이블의 열 하위 집합에서 이러한 집계 쿼리가 더 빠릅니다. TPC-H 벤치 마크 의 데이터를 사용하여 이러한 실험을 직접 시도하고 두 레이아웃의 런타임을 비교할 수 있습니다. 열 지향 데이터베이스 시스템에 대한 위키 백과 기사도 좋습니다.

따라서 데이터베이스 시스템에서 쿼리 워크로드가 미리 알려진 경우 워크로드의 쿼리에 적합한 레이아웃에 데이터를 저장하고 이러한 레이아웃의 데이터에 액세스 할 수 있습니다. 위의 예에서 우리는 열 레이아웃을 생성하고 캐시 친화적이되도록 합계를 계산하도록 코드를 변경했습니다.


1

캐시는 연속 메모리 만 캐시하지는 않습니다. 여러 줄 (최소 4 개)이 있으므로 불연속적이고 겹치는 메모리를 종종 효율적으로 저장할 수 있습니다.

위의 모든 예제에서 누락 된 것은 측정 된 벤치 마크입니다. 성능에 대한 많은 신화가 있습니다. 당신이 그것을 측정하지 않으면 당신은 모른다. 개선 된 부분 이 없다면 코드를 복잡하게 만들지 마십시오 .

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.