" 캐시 비친 화 코드 "와 " 캐시 친화 "코드 의 차이점은 무엇입니까 ?
캐시 효율적인 코드를 작성하려면 어떻게해야합니까?
" 캐시 비친 화 코드 "와 " 캐시 친화 "코드 의 차이점은 무엇입니까 ?
캐시 효율적인 코드를 작성하려면 어떻게해야합니까?
답변:
최신 컴퓨터에서는 가장 낮은 수준의 메모리 구조 ( 레지스터 ) 만 단일 클록 주기로 데이터를 이동할 수 있습니다. 그러나 레지스터는 매우 비싸고 대부분의 컴퓨터 코어에는 수십 개 미만의 레지스터가 있습니다 ( 총 수백에서 수천 바이트 ). 메모리 스펙트럼 ( DRAM ) 의 다른 쪽 끝 에서, 메모리는 매우 저렴하지만 (즉, 문자 그대로 수백만 배 저렴 ) 데이터 수신 요청 후 수백 사이클이 걸린다. 캐시 메모리 는 초고가와 초고가 사이의 간격을 메우기 위해 사용 됩니다속도와 비용이 감소하면서 L1, L2, L3으로 명명되었습니다. 아이디어는 대부분의 실행 코드가 종종 작은 변수 세트에 충돌하고 나머지는 훨씬 더 큰 변수 세트에 자주 충돌한다는 것입니다. 프로세서가 L1 캐시에서 데이터를 찾을 수 없으면 L2 캐시를 찾습니다. 없는 경우 L3 캐시 및없는 경우 주 메모리. 이러한 각각의 "누락"은 시간이 많이 걸립니다.
(시스템 메모리가 너무 하드 디스크 스토리지이므로 캐시 메모리는 시스템 메모리와 유사합니다. 하드 디스크 스토리지는 매우 저렴하지만 매우 느립니다).
캐싱은 대기 시간 의 영향을 줄이는 주요 방법 중 하나입니다 . Herb Sutter의 말을 인용하려면 (아래 링크 참조) : 대역폭을 늘리는 것은 쉽지만 대기 시간을 벗어나는 길을 살 수는 없습니다 .
데이터는 항상 메모리 계층 구조를 통해 검색됩니다 (가장 작은 == 가장 빠름에서 가장 느림). 캐시 적중 / 미스는 일반적으로 CPU의 캐시의 최고 수준의 히트 / 미스를 말한다 - 최고 수준으로 내가 가장 큰 == 느린 것을 의미한다. 캐시 적중률은 모든 캐시 누락으로 인해 많은 시간 (RAM의 경우 수백 사이클, HDD의 경우 수천만 사이클 )이 걸리는 RAM에서 데이터를 가져 오거나 (또는 더 나쁘게) 성능에 중요합니다 . 이에 비해 (가장 높은 수준의) 캐시에서 데이터를 읽는 것은 일반적으로 몇 번의 주기만 걸립니다.
최신 컴퓨터 아키텍처에서 성능 병목 현상은 CPU 다이를 떠나고 있습니다 (예 : RAM에 액세스). 시간이지나면서 악화 될뿐입니다. 프로세서 주파수의 증가는 현재 더 이상 성능 향상과 관련이 없습니다. 문제는 메모리 액세스입니다. 따라서 CPU의 하드웨어 설계 노력은 현재 캐시 최적화, 프리 페치, 파이프 라인 및 동시성에 중점을두고 있습니다. 예를 들어, 최신 CPU는 캐시에 약 85 %의 다이를 사용하고 데이터를 저장 / 이동하는 데 최대 99 %를 소비합니다!
이 주제에 대해 언급해야 할 것이 많이 있습니다. 다음은 캐시, 메모리 계층 및 적절한 프로그래밍에 대한 훌륭한 참고 자료입니다.
캐시 친화적 인 코드의 매우 중요한 측면은 로컬 성의 원칙에 관한 것이며 , 효율적인 캐싱을 위해 관련 데이터를 메모리에 가까이 배치하는 것이 목표입니다. CPU 캐시와 관련하여 캐시 라인을 알고 있어야 작동 방식을 이해할 수 있습니다. 캐시 라인은 어떻게 작동합니까?
캐싱을 최적화하려면 다음과 같은 특정 측면이 매우 중요합니다.
적절한 사용 C ++ 컨테이너
캐시 친화적이고 캐시 비 친화적 인 간단한 예는 다음과 같습니다. C ++의 std::vector
대 std::list
. 의 요소는 std::vector
연속 메모리에 저장되며, 액세스하는 방식은의 요소를 액세스하는 것 보다 캐시에 훨씬 친숙 std::list
합니다. 이것은 공간적 지역성 때문입니다.
이 유튜브 클립 에서 Bjarne Stroustrup이 매우 멋진 그림을 제공 합니다 (링크에 @Mohammad Ali Baydoun에게 감사드립니다!).
데이터 구조 및 알고리즘 설계에서 캐시를 무시하지 마십시오
가능하면 캐시를 최대한 활용할 수있는 방식으로 데이터 구조와 계산 순서를 조정하십시오. 이와 관련하여 일반적인 기술은 캐시 차단 (Archive.org 버전) 인데, 이는 고성능 컴퓨팅 (예 : ATLAS 참조 ) 에서 매우 중요 합니다.
암시적인 데이터 구조 이해 및 활용
현장의 많은 사람들이 때때로 잊어 버린 또 다른 간단한 예는 열 주요입니다 (예 : 포트란,MATLAB) 대 행 순서 순서 (예 : 씨,C ++2 차원 배열을 저장합니다. 예를 들어 다음 매트릭스를 고려하십시오.
1 2
3 4
행 메이저 순서에서 이것은 메모리에 1 2 3 4
; 열 주요 순서에서 이것은로 저장됩니다 1 3 2 4
. 이 순서를 악용하지 않는 구현은 캐시 문제에 빠르게 발생한다는 것을 쉽게 알 수 있습니다. 불행히도, 나는 내 도메인 (기계 학습)에서 이와 같은 것들을 매우 자주 본다 . @MatteoItalia는이 예제를 그의 답변에서 더 자세히 보여주었습니다.
메모리에서 행렬의 특정 요소를 가져올 때 그 근처의 요소도 가져 와서 캐시 라인에 저장됩니다. 순서가 악용되면 메모리 액세스 횟수가 줄어 듭니다 (다음 계산에 필요한 다음 몇 개의 값이 이미 캐시 라인에 있기 때문에).
간단히하기 위해, 캐시는 2 개의 매트릭스 요소를 포함 할 수있는 단일 캐시 라인을 포함하고 주어진 요소가 메모리에서 페치 될 때 다음 요소도 있다고 가정합니다. 위의 예제 2x2 행렬의 모든 요소에 대해 합계를 가져오고 싶다고 가정 해 봅시다 M
.
순서 활용 (예 : 열 색인을 먼저 변경) C ++) :
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
순서를 악용하지 않음 (예 : 행 색인을 먼저 변경) C ++) :
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 덕분에) : 왜 빠른 정렬되지 않은 배열을 처리하는 것보다 정렬 된 배열을 처리?
가상 기능을 피하십시오
문맥 상에 C ++, virtual
방법은 캐시 미스와 관련하여 논란의 여지가있는 문제를 나타냅니다 (성능 측면에서 가능한 경우 피해야한다는 일반적인 합의가 존재합니다). 가상 함수는 룩업 동안 캐시 미스를 유도 할 수 있지만,이 경우에만 발생하는 경우 이 일부에 의해 비 문제로 간주하므로, 특정 기능이 자주 호출되지 않습니다 (그렇지 않으면 가능성이 캐시 될 것이다). 이 문제에 대한 참조 는 C ++ 클래스에서 가상 메소드를 사용하는 데 드는 성능 비용은 얼마입니까?를 확인하십시오.
다중 프로세서 캐시를 사용하는 최신 아키텍처에서 흔히 발생하는 문제를 허위 공유 라고 합니다 . 이것은 각각의 개별 프로세서가 다른 메모리 영역에서 데이터를 사용하려고 시도하고 동일한 캐시 라인에 데이터를 저장하려고 할 때 발생합니다 . 이로 인해 다른 프로세서가 사용할 수있는 데이터가 들어있는 캐시 라인이 반복해서 덮어 쓰기됩니다. 효과적으로 다른 스레드는이 상황에서 캐시 누락을 유도하여 서로를 대기시킵니다. (링크에 @Matt 덕분에) : 캐시 라인 크기에 맞추는 방법과시기?
RAM 메모리에서 캐싱이 제대로 이루어지지 않는 경우 (이 문맥에서는 의미가 없을 수도 있음)를 스 래싱 이라고 합니다. 프로세스가 디스크 액세스가 필요한 페이지 폴트 (예 : 현재 페이지에없는 메모리에 액세스)를 지속적으로 생성 할 때 발생합니다.
@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도 회전하고 나중에 다양한 분석을 수행하여 캐시에 친숙하지 않은 코드를 회전으로 제한하는 것이 좋습니다.
캐시 사용 최적화에는 크게 두 가지 요소가 있습니다.
다른 사람들이 이미 언급 한 첫 번째 요소는 참조의 지역성입니다. 참조의 지역 성은 실제로 공간과 시간의 두 가지 차원을 가지고 있습니다.
공간 차원도 두 가지로 나뉩니다. 첫째, 정보를 조밀하게 압축하여 더 많은 정보가 제한된 메모리에 적합합니다. 이는 예를 들어 포인터로 연결된 작은 노드를 기반으로 데이터 구조를 정당화하기 위해 계산 복잡성을 대폭 개선해야 함을 의미합니다.
둘째, 함께 처리 될 정보도 함께 원합니다. 일반적인 캐시는 "라인"으로 작동합니다. 즉, 일부 정보에 액세스하면 근처 주소의 다른 정보가 터치 한 부분으로 캐시에로드됩니다. 예를 들어, 1 바이트를 터치하면 캐시가 해당 바이트 근처에 128 바이트 또는 256 바이트를로드 할 수 있습니다. 이를 활용하기 위해 일반적으로 동시에로드 된 다른 데이터를 사용할 가능성을 최대화하도록 데이터를 정렬하려고합니다.
아주 간단한 예를 들어, 선형 검색이 예상보다 이진 검색보다 훨씬 경쟁력이 있음을 의미 할 수 있습니다. 캐시 라인에서 하나의 항목을로드하면 해당 캐시 라인의 나머지 데이터를 사용하는 것은 거의 무료입니다. 이진 검색은 액세스하는 캐시 라인 수를 줄일 수있을 정도로 데이터가 클 때만 이진 검색이 훨씬 빨라집니다.
시간 차원은 일부 데이터에 대해 일부 작업을 수행 할 때 해당 데이터에 대한 모든 작업을 한 번에 수행하기를 원한다는 것을 의미합니다.
이 태그를 C ++로 태그 했으므로 비교적 캐시에 친숙하지 않은 디자인의 고전적인 예를 살펴 보겠습니다 std::valarray
. valarray
과부하 대부분의 산술 연산자는, 그래서 (예를 들어) 말할 수있다 a = b + c + d;
(여기서 a
, b
, c
및 d
모든 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"이라는 또 다른 관련 항목이 있습니다. 이는 두 개 이상의 프로세서 / 코어가 별도의 데이터를 가지고 있지만 동일한 캐시 라인에있는 멀티 프로세서 또는 멀티 코어 시스템에서 발생합니다. 이렇게하면 두 개의 프로세서 / 코어가 각각 별도의 데이터 항목이 있더라도 데이터에 대한 액세스를 조정합니다. 특히 두 개가 교대로 데이터를 수정하는 경우 데이터가 프로세서간에 지속적으로 셔틀되어야하므로 막대한 속도 저하가 발생할 수 있습니다. 캐시를보다 "방법"또는 이와 유사한 방식으로 구성하여 쉽게 치료할 수는 없습니다. 이를 방지하는 기본 방법은 두 개의 스레드가 동일한 캐시 라인에있을 수있는 데이터를 거의 수정하지 않는 것이 바람직합니다 (데이터가 할당되는 주소를 제어하는 데 어려움이 있다는 동일한 경고가 있음).
C ++을 잘 아는 사람들은 이것이 표현식 템플릿과 같은 것을 통해 최적화가 가능한지 궁금 할 것입니다. 나는 대답이 그렇다는 것을 확신합니다. 그렇다면 가능할 것입니다. 그렇다면 상당히 상당한 승리 일 것입니다. 그러나 아무도 그렇게하지 않았다는 것을 알지 못하고 조금만 valarray
익숙해지면 적어도 어느 누구도 그렇게하는 것을보고 약간 놀랐습니다.
누군가가 valarray
(성능을 위해 특별히 설계된) 이것이 어떻게 잘못 될 수 있는지 궁금해하는 경우 한 가지로 귀착됩니다. 이것은 실제로 오래된 주 크레이와 같은 기계를 위해 설계되었으며 빠른 주 메모리를 사용하고 캐시는 사용하지 않았습니다. 그들에게는 이것이 거의 이상적인 디자인이었습니다.
그렇습니다. 단순화하고 있습니다. 대부분의 캐시는 실제로 가장 최근에 사용한 항목을 정확하게 측정하지 않지만 각 액세스에 대해 전체 타임 스탬프를 유지하지 않고도 그와 가까운 휴리스틱을 사용합니다.
valarray
예를 좋아합니다 .
데이터 지향 디자인의 세계에 오신 것을 환영합니다. 기본적인 진언은 virtual
더 나은 지역성을 향한 모든 단계 인 분류, 분기 제거, 배치, 통화 제거 입니다.
C ++로 질문에 태그를 지정 했으므로 필수 C ++ Bullshit이 있습니다. Tony Albrecht의 객체 지향 프로그래밍 의 함정 또한이 주제에 대한 훌륭한 소개입니다.
캐시 비친 화 대 캐시 친화적 코드의 전형적인 예는 매트릭스 곱셈의 "캐시 차단"입니다.
순진 행렬 곱셈은 다음과 같습니다.
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
루프 타일링은 매우 밀접한 관련이 있습니다.
k==;
나는 이것이 오타되기를 바라고있다?
오늘날 프로세서는 여러 수준의 계단식 메모리 영역에서 작동합니다. 따라서 CPU에는 CPU 칩 자체에 많은 메모리가 있습니다. 이 메모리에 매우 빠르게 액세스 할 수 있습니다. CPU에 있지 않고 상대적으로 액세스 속도가 느린 시스템 메모리에 도달 할 때까지 각각의 액세스 속도가 느리고 캐시보다 다른 캐시 레벨이 있습니다.
논리적으로 CPU의 명령어 세트에서는 거대한 가상 주소 공간의 메모리 주소 만 참조하면됩니다. 단일 메모리 주소에 액세스하면 CPU가 가져옵니다. 예전에는 단일 주소 만 가져 왔습니다. 그러나 오늘날 CPU는 요청한 비트 주위에 많은 메모리를 가져 와서 캐시에 복사합니다. 특정 주소를 요청하면 가까운 시일 내에 주소를 요청할 가능성이 높다고 가정합니다. 예를 들어 버퍼를 복사하는 경우 연속 주소를 읽고 쓸 수 있습니다.
따라서 오늘 주소를 가져올 때 첫 번째 레벨의 캐시를 검사하여 이미 해당 주소를 캐시로 읽었는지 확인합니다.이를 찾지 못하면 캐시 미스이므로 다음 레벨로 이동해야합니다. 캐시를 찾아 메인 메모리로 나가야 할 때까지 캐시를 찾습니다.
캐시 친화적 인 코드는 액세스를 메모리에서 서로 가깝게 유지하여 캐시 누락을 최소화합니다.
예를 들어, 거대한 2 차원 테이블을 복사하려고한다고 상상해보십시오. 메모리에서 연속으로 도달 행으로 구성되며 한 행은 바로 다음 행을 따릅니다.
왼쪽에서 오른쪽으로 한 번에 한 행씩 요소를 복사 한 경우 캐시 친화적입니다. 한 번에 한 열씩 테이블을 복사하기로 결정한 경우 정확히 같은 양의 메모리를 복사하지만 캐시가 비우호적입니다.
데이터는 캐시 친화적이어야 할뿐만 아니라 코드에도 중요하다는 점을 분명히해야합니다. 이것은 분기 예측, 명령 순서 변경, 실제 구분 및 기타 기술을 피하는 것입니다.
일반적으로 코드가 밀도가 높을수록 코드 저장에 필요한 캐시 라인이 줄어 듭니다. 이로 인해 더 많은 캐시 라인이 데이터에 사용 가능해집니다.
코드는 일반적으로 자체 캐시 라인을 하나 이상 필요로하므로 데이터를위한 캐시 라인이 줄어듦에 따라 모든 곳에서 함수를 호출해서는 안됩니다.
함수는 캐시 라인 정렬 친화적 인 주소에서 시작해야합니다. 이를 위해 (gcc) 컴파일러 스위치가 있지만 기능이 매우 짧은 경우 각 캐시 라인을 점유하는 것은 낭비가 될 수 있습니다. 예를 들어, 가장 자주 사용되는 함수 중 3 개가 하나의 64 바이트 캐시 라인에 들어가는 경우 각 라인에 자체 라인이있는 경우보다 낭비가 적고 다른 용도로는 2 개의 캐시 라인을 사용할 수 없게됩니다. 일반적인 정렬 값은 32 또는 16 일 수 있습니다.
따라서 코드를 조밀하게 만들기 위해 약간의 시간을 투자하십시오. 다른 구문을 테스트하고 생성 된 코드 크기와 프로파일을 컴파일하고 검토하십시오.
@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 벤치 마크 의 데이터를 사용하여 이러한 실험을 직접 시도하고 두 레이아웃의 런타임을 비교할 수 있습니다. 열 지향 데이터베이스 시스템에 대한 위키 백과 기사도 좋습니다.
따라서 데이터베이스 시스템에서 쿼리 워크로드가 미리 알려진 경우 워크로드의 쿼리에 적합한 레이아웃에 데이터를 저장하고 이러한 레이아웃의 데이터에 액세스 할 수 있습니다. 위의 예에서 우리는 열 레이아웃을 생성하고 캐시 친화적이되도록 합계를 계산하도록 코드를 변경했습니다.