이것은 주관적인 질문처럼 들릴 수 있지만, 내가 찾고있는 것은이와 관련하여 발생할 수있는 특정 사례입니다.
-
코드를 효율적으로 만들고 캐시를 효율적으로 / 캐시 친화적으로 만드는 방법 두 가지 관점에서, 데이터 캐시 및 프로그램 캐시 (명령 캐시), 즉 데이터 구조 및 코드 구성과 관련하여 코드에서 어떤 것이 캐시에 효과적이되도록 처리해야 하는가.
-
코드 캐시를 효과적으로 사용하기 위해 사용하거나 피해야하는 특정 데이터 구조가 있거나 해당 구조의 멤버 등에 액세스하는 특정 방법이 있습니까?
-
프로그램 구성 (if, for, switch, break, goto, …), 코드 흐름 (if 내부, if 내부 if 등 …) 이이 문제에서 따라야하거나 피해야합니까?
캐시 효율적인 코드를 만드는 데 관련된 개별 경험을 듣고 싶습니다. 프로그래밍 언어 (C, C ++, Assembly, …), 하드웨어 대상 (ARM, Intel, PowerPC 등), OS (Windows, Linux, S ymbian 등) 등이 될 수 있습니다. .
다양성은 그것을 깊이 이해하는 데 도움이 될 것입니다.
답변
캐시는 CPU가 메모리 요청이 이행 될 때까지 대기하는 횟수를 줄이고 (메모리 대기 시간을 피함 ) 두 번째 효과로, 전송해야하는 전체 데이터 양을 줄일 수 있습니다 (보존 메모리 대역폭 ).
메모리 페치 대기 시간으로 인한 고통을 피하는 기술은 일반적으로 가장 먼저 고려해야 할 사항이며 때로는 먼 길을 돕습니다. 제한된 메모리 대역폭은 특히 많은 스레드가 메모리 버스를 사용하려는 다중 코어 및 다중 스레드 응용 프로그램의 경우 제한 요소입니다. 후자의 문제를 해결하는 데 도움이되는 다양한 기술이 있습니다.
공간적 지역성을 개선 한다는 것은 각 캐시 라인이 캐시에 매핑 된 후에 전체적으로 사용되도록 보장한다는 것을 의미합니다. 다양한 표준 벤치 마크를 살펴보면, 캐시 라인이 제거되기 전에 놀랍도록 많은 부분이 페치 된 캐시 라인을 100 % 사용하지 못한다는 것을 알았습니다.
캐시 라인 활용도를 높이면 세 가지 측면에서 도움이됩니다.
- 캐시에 더 유용한 데이터를 넣는 경향이있어 유효 캐시 크기가 증가합니다.
- 동일한 캐시 라인에 더 유용한 데이터를 맞추는 경향이있어 요청 된 데이터가 캐시에서 발견 될 가능성이 높아집니다.
- 페치가 적으므로 메모리 대역폭 요구 사항이 줄어 듭니다.
일반적인 기술은 다음과 같습니다.
- 더 작은 데이터 유형 사용
- 정렬 구멍을 피하기 위해 데이터를 구성하십시오 (크기를 줄임으로써 구조체 멤버를 정렬하는 것이 한 가지 방법입니다)
- 표준 동적 메모리 할당자를 조심하십시오. 예열되면 구멍이 생겨 데이터가 메모리에 퍼질 수 있습니다.
- 모든 인접 데이터가 실제로 핫 루프에서 사용되는지 확인하십시오. 그렇지 않으면, 핫 루프가 핫 데이터를 사용하도록 데이터 구조를 핫 및 콜드 구성 요소로 분리하는 것을 고려하십시오.
- 불규칙한 액세스 패턴을 나타내는 알고리즘 및 데이터 구조를 피하고 선형 데이터 구조를 선호하십시오.
또한 캐시를 사용하는 것보다 메모리 대기 시간을 숨길 수있는 다른 방법이 있습니다.
최신 CPU : 종종 하나 이상의 하드웨어 프리 페 처가 있습니다. 그들은 캐시에서 미스를 훈련시키고 규칙 성을 발견하려고 노력합니다. 예를 들어, 후속 캐시 라인을 몇 번 놓친 후 hw 프리 페처는 애플리케이션의 요구를 예상하여 캐시 라인을 캐시로 가져 오기 시작합니다. 정기적 인 액세스 패턴이있는 경우 하드웨어 프리 페처는 일반적으로 매우 잘 수행됩니다. 또한 프로그램에 정기적 인 액세스 패턴이 표시되지 않으면 프리 페치 명령어를 직접 추가하여 상황을 개선 할 수 있습니다 .
캐시에서 항상 그리워하는 명령이 서로 가깝게 발생하도록 명령어를 다시 그룹화하면 CPU가 때때로 이러한 페치와 겹치므로 애플리케이션이 하나의 대기 시간 히트 ( 메모리 레벨 병렬 처리 ) 만 유지할 수 있습니다.
전체 메모리 버스 압력을 줄이려면 temporal locality 라는 문제를 해결해야 합니다. 이는 데이터가 여전히 캐시에서 제거되지 않은 동안 데이터를 재사용해야 함을 의미합니다.
동일한 데이터에 닿는 루프를 병합 ( 루프 융합 )하고 모든 타일링 또는 블로킹 이라는 재 작성 기술을 사용 하면 이러한 추가 메모리 페치를 피하기 위해 노력합니다.
이 재 작성 연습에는 몇 가지 규칙이 있지만 일반적으로 프로그램의 의미에 영향을 미치지 않도록 루프 전달 데이터 종속성을 신중하게 고려해야합니다.
이러한 것들이 멀티 코어 세계에서 실제로 지불하는 것인데, 일반적으로 두 번째 스레드를 추가 한 후에는 처리량이 크게 향상되지 않습니다.
답변
이에 대한 답변이 더 이상 없다고 믿을 수 없습니다. 어쨌든 고전적인 예는 다차원 배열을 “내부”로 반복하는 것입니다.
pseudocode
for (i = 0 to size)
for (j = 0 to size)
do something with ary[j][i]
이것이 캐시 비효율적 인 이유는 최신 CPU가 단일 메모리 주소에 액세스 할 때 주 메모리에서 “가까운”메모리 주소로 캐시 라인을로드하기 때문입니다. 내부 루프의 배열에서 “j”(외부) 행을 반복하므로 내부 루프를 통과 할 때마다 캐시 라인이 플러시되고 [ j] [i] 항목. 이것이 동등한 것으로 변경되면 :
for (i = 0 to size)
for (j = 0 to size)
do something with ary[i][j]
훨씬 빠르게 실행됩니다.
답변
기본 규칙은 실제로 매우 간단합니다. 까다로운 곳은 코드에 어떻게 적용되는지입니다.
캐시는 두 가지 원칙, 즉 임시 지역 및 공간 지역에서 작동합니다. 전자는 최근에 특정 데이터 청크를 사용한 경우 곧 다시 필요할 것이라는 생각입니다. 후자는 최근에 주소 X의 데이터를 사용한 경우 곧 주소 X + 1이 필요할 것입니다.
캐시는 가장 최근에 사용 된 데이터 청크를 기억하여이를 수용하려고합니다. 캐시 라인 (일반적으로 128 바이트 정도)으로 작동하므로 단일 바이트 만 필요한 경우에도이를 포함하는 전체 캐시 라인이 캐시로 가져옵니다. 따라서 이후에 다음 바이트가 필요한 경우 이미 캐시에 있습니다.
그리고 이것은 항상 여러분 자신의 코드가이 두 가지 형태의 지역성을 최대한 활용하기를 원한다는 것을 의미합니다. 메모리 전체를 뛰어 넘지 마십시오. 하나의 작은 영역에서 최대한 많은 작업을 수행 한 후 다음 영역으로 이동하여 최대한 많은 작업을 수행하십시오.
간단한 예는 1800의 답변이 보여준 2D 배열 순회입니다. 한 번에 한 행씩 이동하면 메모리를 순차적으로 읽는 것입니다. 열 단위로 수행하면 한 항목을 읽은 다음 완전히 다른 위치 (다음 행의 시작)로 이동하고 한 항목을 읽고 다시 점프합니다. 그리고 마지막으로 첫 번째 행으로 돌아 가면 더 이상 캐시에 없습니다.
코드에도 동일하게 적용됩니다. 점프 또는 분기는 캐시 사용이 덜 효율적임을 의미합니다 (명령을 순차적으로 읽지 않고 다른 주소로 점프하기 때문에). 물론 작은 if 문은 아무것도 변경하지 않을 것입니다 (몇 바이트를 건너 뛰기 때문에 캐시 된 영역 내부에서 끝날 것입니다). 함수 호출은 일반적으로 완전히 다른 것으로 점프 함을 암시합니다 캐시되지 않은 주소. 최근에 전화하지 않았다면.
명령 캐시 사용은 일반적으로 문제가 훨씬 적습니다. 일반적으로 걱정해야 할 것은 데이터 캐시입니다.
구조체 나 클래스에서 모든 멤버는 연속적으로 배치됩니다. 배열에서 모든 항목은 연속적으로 배치됩니다. 링크 된 목록에서 각 노드는 완전히 다른 위치에 할당되며 이는 나쁩니다. 일반적으로 포인터는 관련이없는 주소를 가리키는 경향이 있으며,이 주소를 역 참조하면 캐시가 누락 될 수 있습니다.
그리고 여러 코어를 활용하려는 경우 일반적으로 한 번에 하나의 CPU 만 L1 캐시에 지정된 주소를 가질 수 있으므로 매우 흥미로울 수 있습니다. 따라서 두 코어가 지속적으로 동일한 주소에 액세스하면 주소를 놓고 싸우면서 캐시가 계속 누락됩니다.
답변
메모리와 소프트웨어가 어떻게 상호 작용하는지에 관심이 있다면 Ulrich Drepper 가 메모리 에 대해 알아야 할 9 부로 구성된 기사를 읽는 것이 좋습니다 . 104 페이지 PDF 로도 제공됩니다 .
이 질문과 특히 관련된 섹션은 Part 2 (CPU 캐시) 및 Part 5 (프로그래머가 수행 할 수있는 작업-캐시 최적화) 일 수 있습니다.
답변
캐시에 적합한 코드의 주요 요소는 데이터 액세스 패턴 외에도 데이터 크기 입니다. 데이터가 적을수록 캐시에 더 많은 데이터가 들어갑니다.
이것은 주로 메모리 정렬 데이터 구조의 요소입니다. “전통적인”지혜는 CPU가 전체 단어에만 액세스 할 수 있고 단어에 둘 이상의 값이 포함 된 경우 추가 작업 (간단한 쓰기 대신 읽기-쓰기 쓰기)을 수행해야하므로 데이터 구조는 단어 경계에 정렬되어야한다고 말합니다. . 그러나 캐시는이 인수를 완전히 무효화 할 수 있습니다.
마찬가지로 Java 부울 배열은 개별 값에서 직접 조작 할 수 있도록 각 값에 대해 전체 바이트를 사용합니다. 실제 비트를 사용하는 경우 데이터 크기를 8 배로 줄일 수 있지만 개별 값에 대한 액세스는 훨씬 복잡해져 비트 이동 및 마스크 작업 BitSet
이 필요합니다 ( 클래스가이를 수행함). 그러나 캐시 효과로 인해 배열이 클 때 부울 []을 사용하는 것보다 훨씬 빠릅니다. IIRC I은 이런 식으로 2 배 또는 3 배의 속도 향상을 달성했습니다.
답변
캐시에 가장 효과적인 데이터 구조는 배열입니다. CPU가 주 메모리에서 한 번에 전체 캐시 라인 (일반적으로 32 바이트 이상)을 읽을 때 데이터 구조가 순차적으로 배치되는 경우 캐시가 가장 잘 작동합니다.
무작위 순서로 메모리에 액세스하는 알고리즘은 무작위로 액세스되는 메모리를 수용하기 위해 항상 새로운 캐시 라인이 필요하기 때문에 캐시를 폐기합니다. 반면에 배열을 통해 순차적으로 실행되는 알고리즘은 다음과 같은 이유로 가장 좋습니다.
-
예를 들어 추론 적으로 더 많은 메모리를 캐시에 넣은 후 나중에 액세스 할 수있는 기회를 CPU에 제공합니다. 이 미리 읽기 기능은 성능을 크게 향상시킵니다.
-
큰 어레이에서 엄격한 루프를 실행하면 CPU가 루프에서 실행되는 코드를 캐시 할 수 있으며 대부분의 경우 외부 메모리 액세스를 차단하지 않고도 캐시 메모리에서 알고리즘을 완전히 실행할 수 있습니다.
답변
게임 엔진에서 사용한 한 가지 예는 데이터를 객체에서 자신의 배열로 옮기는 것입니다. 물리에 영향을받은 게임 오브젝트에는 다른 많은 데이터가 첨부되어있을 수 있습니다. 그러나 물리 업데이트 루프 동안 모든 엔진은 위치, 속도, 질량, 바운딩 박스 등에 관한 데이터였습니다. 따라서 모든 것이 자체 배열에 배치되고 SSE에 대해 최대한 최적화되었습니다.
물리 루프 동안 물리 데이터는 벡터 수학을 사용하여 배열 순서로 처리되었습니다. 게임 오브젝트는 오브젝트 배열을 다양한 배열의 인덱스로 사용했습니다. 배열을 재배치해야 할 경우 포인터가 무효화 될 수 있으므로 포인터가 아닙니다.
여러 가지 방법으로이 객체 지향 디자인 패턴을 위반했지만 동일한 루프에서 작동해야하는 데이터를 서로 가깝게 배치하여 코드를 훨씬 빠르게 만들었습니다.
대부분의 현대 게임은 Havok과 같은 사전 빌드 된 물리 엔진을 사용하기 때문에이 예제는 아마도 오래되었을 것입니다.