CPU 캐시를 가장 잘 활용하여 성능을 향상시키는 코드는 어떻게 작성합니까?


159

이것은 주관적인 질문처럼 들릴 수 있지만, 내가 찾고있는 것은이와 관련하여 발생할 수있는 특정 사례입니다.

  1. 코드를 효율적으로 만들고 캐시를 효율적으로 / 캐시 친화적으로 만드는 방법 두 가지 관점에서, 데이터 캐시 및 프로그램 캐시 (명령 캐시), 즉 데이터 구조 및 코드 구성과 관련하여 코드에서 어떤 것이 캐시에 효과적이되도록 처리해야 하는가.

  2. 코드 캐시를 효과적으로 사용하기 위해 사용하거나 피해야하는 특정 데이터 구조가 있거나 해당 구조의 멤버 등에 액세스하는 특정 방법이 있습니까?

  3. 프로그램 구성 (if, for, switch, break, goto, ...), 코드 흐름 (if 내부, if 내부 if 등 ...) 이이 문제에서 따라야하거나 피해야합니까?

캐시 효율적인 코드를 만드는 데 관련된 개별 경험을 듣고 싶습니다. 프로그래밍 언어 (C, C ++, Assembly, ...), 하드웨어 대상 (ARM, Intel, PowerPC 등), OS (Windows, Linux, S ymbian 등) 등이 될 수 있습니다. .

다양성은 그것을 깊이 이해하는 데 도움이 될 것입니다.


1
이 이야기 소개 개요 좋은 제공으로 youtu.be/BP6NxVxDQIs
schoetbi

위의 단축 된 URL이 더 이상 작동하지 않는 것 같습니다. youtube.com/watch?v=BP6NxVxDQIs
Abhinav Upadhyay

답변:


119

캐시는 CPU가 메모리 요청이 이행 될 때까지 대기하는 횟수를 줄이고 (메모리 대기 시간을 피함 ) 두 번째 효과로, 전송해야하는 전체 데이터 양을 줄일 수 있습니다 (보존 메모리 대역폭 ).

메모리 페치 대기 시간으로 인한 고통을 피하는 기술은 일반적으로 가장 먼저 고려해야 할 사항이며 때로는 먼 길을 돕습니다. 제한된 메모리 대역폭은 특히 많은 스레드가 메모리 버스를 사용하려는 다중 코어 및 다중 스레드 응용 프로그램의 경우 제한 요소입니다. 후자의 문제를 해결하는 데 도움이되는 다양한 기술이 있습니다.

공간적 지역성을 개선 한다는 것은 각 캐시 라인이 캐시에 매핑 된 후에 전체적으로 사용되도록 보장한다는 것을 의미합니다. 다양한 표준 벤치 마크를 살펴보면, 캐시 라인이 제거되기 전에 놀랍도록 많은 부분이 페치 된 캐시 라인을 100 % 사용하지 못한다는 것을 알았습니다.

캐시 라인 활용도를 높이면 세 가지 측면에서 도움이됩니다.

  • 캐시에 더 유용한 데이터를 넣는 경향이있어 유효 캐시 크기가 증가합니다.
  • 동일한 캐시 라인에 더 유용한 데이터를 맞추는 경향이있어 요청 된 데이터가 캐시에서 발견 될 가능성이 높아집니다.
  • 페치가 적으므로 메모리 대역폭 요구 사항이 줄어 듭니다.

일반적인 기술은 다음과 같습니다.

  • 더 작은 데이터 유형 사용
  • 정렬 구멍을 피하기 위해 데이터를 구성하십시오 (크기를 줄임으로써 구조체 멤버를 정렬하는 것이 한 가지 방법입니다)
  • 표준 동적 메모리 할당자를 조심하십시오. 예열되면 구멍이 생겨 데이터가 메모리에 퍼질 수 있습니다.
  • 모든 인접 데이터가 실제로 핫 루프에서 사용되는지 확인하십시오. 그렇지 않으면, 핫 루프가 핫 데이터를 사용하도록 데이터 구조를 핫 및 콜드 구성 요소로 분리하는 것을 고려하십시오.
  • 불규칙한 액세스 패턴을 나타내는 알고리즘 및 데이터 구조를 피하고 선형 데이터 구조를 선호하십시오.

또한 캐시를 사용하는 것보다 메모리 대기 시간을 숨길 수있는 다른 방법이 있습니다.

최신 CPU : 종종 하나 이상의 하드웨어 프리 페 처가 있습니다. 그들은 캐시에서 미스를 훈련시키고 규칙 성을 발견하려고 노력합니다. 예를 들어, 후속 캐시 라인을 몇 번 놓친 후 hw 프리 페처는 애플리케이션의 요구를 예상하여 캐시 라인을 캐시로 가져 오기 시작합니다. 정기적 인 액세스 패턴이있는 경우 하드웨어 프리 페처는 일반적으로 매우 잘 수행됩니다. 또한 프로그램에 정기적 인 액세스 패턴이 표시되지 않으면 프리 페치 명령어를 직접 추가하여 상황을 개선 할 수 있습니다 .

캐시에서 항상 그리워하는 명령이 서로 가깝게 발생하도록 명령어를 다시 그룹화하면 CPU가 때때로 이러한 페치와 겹치므로 애플리케이션이 하나의 대기 시간 히트 ( 메모리 레벨 병렬 처리 ) 만 유지할 수 있습니다.

전체 메모리 버스 압력을 줄이려면 temporal locality 라는 문제를 해결해야 합니다. 이는 데이터가 여전히 캐시에서 제거되지 않은 동안 데이터를 재사용해야 함을 의미합니다.

동일한 데이터에 닿는 루프를 병합 ( 루프 융합 )하고 모든 타일링 또는 블로킹 이라는 재 작성 기술을 사용 하면 이러한 추가 메모리 페치를 피하기 위해 노력합니다.

이 재 작성 연습에는 몇 가지 규칙이 있지만 일반적으로 프로그램의 의미에 영향을 미치지 않도록 루프 전달 데이터 종속성을 신중하게 고려해야합니다.

이러한 것들이 멀티 코어 세계에서 실제로 지불하는 것인데, 일반적으로 두 번째 스레드를 추가 한 후에는 처리량이 크게 향상되지 않습니다.


5
다양한 표준 벤치 마크를 살펴보면, 캐시 라인이 제거되기 전에 놀랍도록 많은 부분이 페치 된 캐시 라인을 100 % 사용하지 못한다는 것을 알았습니다. 어떤 종류의 프로파일 링 도구가 이런 종류의 정보를 제공하는지, 어떻게 물어 볼까요?
드래곤 에너지

"정렬 구멍을 피하기 위해 데이터를 정리하십시오 (크기를 줄임으로써 구조체 멤버를 정렬하는 것이 한 방법입니다)"-왜 컴파일러가이를 최적화하지 않습니까? 왜 컴파일러가 항상 "크기를 줄여서 멤버를 정렬 할 수는 없습니까?" 정렬되지 않은 멤버를 유지하면 어떤 이점이 있습니까?
javapowered

나는 기원을 알지 못하지만 하나의 경우, 회원 구조는 웹을 통해 바이트 단위로 전체 구조를 보내고 싶을 수있는 네트워크 통신에서 중요합니다.
Kobrar

1
@javapowered 컴파일러가 언어에 따라 그렇게 할 수는 있지만 확실하지는 않습니다. C에서 할 수없는 이유는 이름이 아닌 기본 주소 + 오프셋으로 멤버를 주소 지정하는 것이 완벽하기 때문에 멤버를 재정렬하면 프로그램이 완전히 중단 될 수 있기 때문입니다.
Dan Bechard

56

이에 대한 답변이 더 이상 없다고 믿을 수 없습니다. 어쨌든 고전적인 예는 다차원 배열을 "내부"로 반복하는 것입니다.

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]

훨씬 빠르게 실행됩니다.


9
대학으로 돌아가서 우리는 행렬 곱셈에 대한 과제를 가졌습니다. "열"행렬을 먼저 바꾸고 정확한 이유로 열을 행이 아닌 행으로 곱하는 것이 더 빠르다는 것이 밝혀졌습니다.
ykaganovich 2016 년

11
실제로, 대부분의 현대 컴파일러는 (최적화를 켠 상태에서) 자체에 의해 이것을 알아낼 수 있습니다
Ricardo Nolde

1
@ykaganovich 또한 Ulrich Dreppers 기사의 예제이기도합니다. lwn.net/Articles/255364
Simon Stender Boisen

이것이 항상 올바른지 확신하지 못합니다. 전체 어레이가 L1 캐시 (대개 32k!)에 들어가면 두 주문 모두 동일한 수의 캐시 적중과 누락이 발생합니다. 아마도 메모리 프리 페칭은 약간의 영향을 줄 수 있습니다. 물론 정정 되서 기쁘다.
Matt Parkins

순서가 중요하지 않은 경우 누가이 코드의 첫 번째 버전을 선택합니까?
silver_rocket

45

기본 규칙은 실제로 매우 간단합니다. 까다로운 곳은 코드에 어떻게 적용되는지입니다.

캐시는 두 가지 원칙, 즉 임시 지역 및 공간 지역에서 작동합니다. 전자는 최근에 특정 데이터 청크를 사용한 경우 곧 다시 필요할 것이라는 생각입니다. 후자는 최근에 주소 X의 데이터를 사용한 경우 곧 주소 X + 1이 필요할 것입니다.

캐시는 가장 최근에 사용 된 데이터 청크를 기억하여이를 수용하려고합니다. 캐시 라인 (일반적으로 128 바이트 정도)으로 작동하므로 단일 바이트 만 필요한 경우에도이를 포함하는 전체 캐시 라인이 캐시로 가져옵니다. 따라서 이후에 다음 바이트가 필요한 경우 이미 캐시에 있습니다.

그리고 이것은 항상 여러분 자신의 코드가이 두 가지 형태의 지역성을 최대한 활용하기를 원한다는 것을 의미합니다. 메모리 전체를 뛰어 넘지 마십시오. 하나의 작은 영역에서 최대한 많은 작업을 수행 한 후 다음 영역으로 이동하여 최대한 많은 작업을 수행하십시오.

간단한 예는 1800의 답변이 보여준 2D 배열 순회입니다. 한 번에 한 행씩 이동하면 메모리를 순차적으로 읽는 것입니다. 열 단위로 수행하면 한 항목을 읽은 다음 완전히 다른 위치 (다음 행의 시작)로 이동하고 한 항목을 읽고 다시 점프합니다. 그리고 마지막으로 첫 번째 행으로 돌아 가면 더 이상 캐시에 없습니다.

코드에도 동일하게 적용됩니다. 점프 또는 분기는 캐시 사용이 덜 효율적임을 의미합니다 (명령을 순차적으로 읽지 않고 다른 주소로 점프하기 때문에). 물론 작은 if 문은 아무것도 변경하지 않을 것입니다 (몇 바이트를 건너 뛰기 때문에 캐시 된 영역 내부에서 끝날 것입니다). 함수 호출은 일반적으로 완전히 다른 것으로 점프 함을 암시합니다 캐시되지 않은 주소. 최근에 전화하지 않았다면.

명령 캐시 사용은 일반적으로 문제가 훨씬 적습니다. 일반적으로 걱정해야 할 것은 데이터 캐시입니다.

구조체 나 클래스에서 모든 멤버는 연속적으로 배치됩니다. 배열에서 모든 항목은 연속적으로 배치됩니다. 링크 된 목록에서 각 노드는 완전히 다른 위치에 할당되며 이는 나쁩니다. 일반적으로 포인터는 관련이없는 주소를 가리키는 경향이 있으며,이 주소를 역 참조하면 캐시가 누락 될 수 있습니다.

그리고 여러 코어를 활용하려는 경우 일반적으로 한 번에 하나의 CPU 만 L1 캐시에 지정된 주소를 가질 수 있으므로 매우 흥미로울 수 있습니다. 따라서 두 코어가 지속적으로 동일한 주소에 액세스하면 주소를 놓고 싸우면서 캐시가 계속 누락됩니다.


4
+1, 좋고 실용적인 조언. 한 가지 추가 사항 : 시간 지역 및 공간 지역은 결합하여 예를 들어 매트릭스 연산의 경우 캐시 라인에 완전히 맞는 행이나 열이 캐시 라인에 맞는 더 작은 행렬로 분할하는 것이 좋습니다. 나는 다차원의 시각화를 위해 그렇게 한 것을 기억합니다. 데이터. 바지에 약간의 킥을 제공했습니다. 캐시는 하나 이상의 '라인'을 보유한다는 것을 기억하는 것이 좋습니다.)
AndreasT

1
한 번에 하나의 CPU 만 L1 캐시에 주어진 주소를 가질 수 있다고 말합니다. 주소가 아닌 캐시 라인을 의미한다고 가정합니다. 또한 적어도 하나의 CPU가 쓰기를 할 때 허위 공유 문제가 있다고 들었지만 둘 다 읽기만하는 경우는 아닙니다. 따라서 '접근'이라는 말은 실제로 쓰기를 의미합니까?
Joseph Garvin

2
@JosephGarvin : 그렇습니다. 맞습니다. 여러 코어가 동시에 L1 캐시에 동일한 캐시 라인을 가질 수 있지만 한 코어가 이러한 주소에 쓰면 다른 모든 L1 캐시에서 무효화되고 다시 수행해야 할 수 있습니다 그것으로 무엇이든. 부정확 한 (잘못된) 문구로 죄송합니다. :)
jalf

44

메모리와 소프트웨어가 어떻게 상호 작용하는지에 관심이 있다면 Ulrich Drepper 가 메모리대해 알아야 할 9 부로 구성된 기사를 읽는 것이 좋습니다 . 104 페이지 PDF 로도 제공됩니다 .

이 질문과 특히 관련된 섹션은 Part 2 (CPU 캐시) 및 Part 5 (프로그래머가 수행 할 수있는 작업-캐시 최적화) 일 수 있습니다.


16
기사의 요점에 대한 요약을 추가해야합니다.
Azmisov

잘 읽어야하지만 여기서 반드시 언급해야 할 또 다른 책은 Hennessy, Patterson, Computer Architecture, A Quantitiative Approach 입니다.
Haymo Kutschbach

15

캐시에 적합한 코드의 주요 요소는 데이터 액세스 패턴 외에도 데이터 크기 입니다. 데이터가 적을수록 캐시에 더 많은 데이터가 들어갑니다.

이것은 주로 메모리 정렬 데이터 구조의 요소입니다. "전통적인"지혜는 CPU가 전체 단어에만 액세스 할 수 있고 단어에 둘 이상의 값이 포함 된 경우 추가 작업 (간단한 쓰기 대신 읽기-쓰기 쓰기)을 수행해야하므로 데이터 구조는 단어 경계에 정렬되어야한다고 말합니다. . 그러나 캐시는이 인수를 완전히 무효화 할 수 있습니다.

마찬가지로 Java 부울 배열은 개별 값에서 직접 조작 할 수 있도록 각 값에 대해 전체 바이트를 사용합니다. 실제 비트를 사용하는 경우 데이터 크기를 8 배로 줄일 수 있지만 개별 값에 대한 액세스는 훨씬 복잡해져 비트 이동 및 마스크 작업 BitSet이 필요합니다 ( 클래스가이를 수행함). 그러나 캐시 효과로 인해 배열이 클 때 부울 []을 사용하는 것보다 훨씬 빠릅니다. IIRC I은 이런 식으로 2 배 또는 3 배의 속도 향상을 달성했습니다.


9

캐시에 가장 효과적인 데이터 구조는 배열입니다. CPU가 주 메모리에서 한 번에 전체 캐시 라인 (일반적으로 32 바이트 이상)을 읽을 때 데이터 구조가 순차적으로 배치되는 경우 캐시가 가장 잘 작동합니다.

무작위 순서로 메모리에 액세스하는 알고리즘은 무작위로 액세스되는 메모리를 수용하기 위해 항상 새로운 캐시 라인이 필요하기 때문에 캐시를 폐기합니다. 반면에 배열을 통해 순차적으로 실행되는 알고리즘은 다음과 같은 이유로 가장 좋습니다.

  1. 예를 들어 추론 적으로 더 많은 메모리를 캐시에 넣은 후 나중에 액세스 할 수있는 기회를 CPU에 제공합니다. 이 미리 읽기 기능은 성능을 크게 향상시킵니다.

  2. 큰 어레이에서 엄격한 루프를 실행하면 CPU가 루프에서 실행되는 코드를 캐시 할 수 있으며 대부분의 경우 외부 메모리 액세스를 차단하지 않고도 캐시 메모리에서 알고리즘을 완전히 실행할 수 있습니다.


@Grover : 포인트 2에 대해. 그래서 단단한 루프 내부에서 각 루프 카운트마다 함수가 호출되면 새로운 코드를 모두 가져 와서 대신 캐시 미스를 유발할 수 있다고 말할 수 있습니까? for 루프 자체의 코드, 함수 호출 없음, 적은 캐시 누락으로 인해 더 빠를까요?
goldenmean

1
예, 아니오 새로운 기능이 캐시에로드됩니다. 캐시 공간이 충분하면 두 번째 반복시 이미 캐시에 해당 기능이 있으므로 다시로드 할 이유가 없습니다. 따라서 첫 번째 통화에서 인기가 있습니다. C / C ++에서는 적절한 세그먼트를 사용하여 서로 바로 옆에 함수를 배치하도록 컴파일러에 요청할 수 있습니다.
grover 2009

한 가지 더 참고 : 루프를 호출하고 캐시 공간이 충분하지 않으면 새로운 함수가 캐시에로드됩니다. 원래 루프가 캐시에서 던져 질 수도 있습니다. 이 경우 호출은 각 반복에 대해 최대 3 개의 페널티가 발생합니다. 하나는 호출 대상을로드하고 다른 하나는 루프를 다시로드합니다. 그리고 루프 헤드가 콜 리턴 주소와 동일한 캐시 라인에없는 경우 세 번째입니다. 이 경우 루프 헤드로 점프하면 새로운 메모리 액세스가 필요합니다.
grover

8

게임 엔진에서 사용한 한 가지 예는 데이터를 객체에서 자신의 배열로 옮기는 것입니다. 물리에 영향을받은 게임 오브젝트에는 다른 많은 데이터가 첨부되어있을 수 있습니다. 그러나 물리 업데이트 루프 동안 모든 엔진은 위치, 속도, 질량, 바운딩 박스 등에 관한 데이터였습니다. 따라서 모든 것이 자체 배열에 배치되고 SSE에 대해 최대한 최적화되었습니다.

물리 루프 동안 물리 데이터는 벡터 수학을 사용하여 배열 순서로 처리되었습니다. 게임 오브젝트는 오브젝트 배열을 다양한 배열의 인덱스로 사용했습니다. 배열을 재배치해야 할 경우 포인터가 무효화 될 수 있으므로 포인터가 아닙니다.

여러 가지 방법으로이 객체 지향 디자인 패턴을 위반했지만 동일한 루프에서 작동해야하는 데이터를 서로 가깝게 배치하여 코드를 훨씬 빠르게 만들었습니다.

대부분의 현대 게임은 Havok과 같은 사전 빌드 된 물리 엔진을 사용하기 때문에이 예제는 아마도 오래되었을 것입니다.


2
+1 전혀 오래되지 않았습니다. 이것은 게임 엔진에 대한 데이터를 구성하는 가장 좋은 방법입니다-데이터 블록을 인접하게 만들고 캐시 근접성 / 지역성을 활용하기 위해 다음 (물리학)으로 이동하기 전에 주어진 유형의 작업 (AI)을 모두 수행하십시오. 참고.
엔지니어 :

몇 주 전 어딘가의 비디오 에서이 정확한 예를 보았지만 그 링크를 잃어 버렸거나 그것을 찾는 방법을 기억할 수 없습니다. 이 예를 본 곳을 기억하십니까?
됩니다

@ 윌 : 아니오, 이것이 어디에 있었는지 정확히 기억하지 못합니다.
Zan Lynx

이것은 엔터티 구성 요소 시스템에 대한 아이디어입니다 (ECS : en.wikipedia.org/wiki/Entity_component_system ). OOP 사례에서 권장하는보다 전통적인 배열 대신 데이터를 배열로 저장하십시오.
BuschnicK

7

한 게시물 만 다루었지만 프로세스간에 데이터를 공유 할 때 큰 문제가 발생합니다. 여러 프로세스가 동일한 캐시 라인을 동시에 수정하려고하지 않도록합니다. 여기서 주목해야 할 것은 "거짓"공유입니다. 여기서 두 개의 인접한 데이터 구조는 캐시 라인을 공유하고 하나를 수정하면 다른 하나에 대한 캐시 라인이 무효화됩니다. 이로 인해 다중 프로세서 시스템에서 데이터를 공유하는 프로세서 캐시간에 캐시 라인이 불필요하게 앞뒤로 이동할 수 있습니다. 이를 피하는 방법은 데이터 구조를 정렬하고 채워서 다른 행에 배치하는 것입니다.


7

사용자 1800 INFORMATION 의 "클래식 예제"에 대한 설명 (너무 긴 의견)

두 반복 순서 ( "outter"및 "inner")의 시간 차이를 확인하고 싶기 때문에 큰 2D 배열로 간단한 실험을했습니다.

measure::start();
for ( int y = 0; y < N; ++y )
for ( int x = 0; x < N; ++x )
    sum += A[ x + y*N ];
measure::stop();

for루프가 교체 된 두 번째 경우 .

느린 버전 ( "x first")은 0.88 초이고 더 빠른 버전은 0.06 초입니다. 그것이 캐싱의 힘입니다 :)

나는 사용 gcc -O2했지만 여전히 루프가 최적화 되지 않았습니다 . 에 의해 코멘트 리카르도 "현대 컴파일러의 대부분이 itselves하여이를 알아낼 수"고 보류를하지 않습니다


확실하지 않습니다. 두 예제 모두 여전히 for 루프의 각 변수에 액세스하고 있습니다. 한 가지 방법이 다른 방법보다 빠른 이유는 무엇입니까?
ed-

그것이 미치는 영향을 이해하는 데있어 궁극적으로 직관적입니다 :)
Laie

@EdwardCorlew 액세스 순서 때문입니다. y- 순서는 데이터에 순차적으로 액세스하기 때문에 더 빠릅니다. 첫 번째 항목이 요청되면 L1 캐시는 전체 캐시 라인을로드합니다. 여기에는 요청 된 int와 다음 15를 더한 것 (64 바이트 캐시 라인 가정)이 포함되어 있으므로 다음 15를 기다리는 CPU 정지가 없습니다. x 액세스 된 요소가 순차적이 아니기 때문에 첫 번째 순서가 느리고, 아마도 N은 액세스중인 메모리가 항상 L1 캐시 외부에있을만큼 충분히 커서 모든 작업이 중단됩니다.
Matt Parkins

4

C ++ 세계에서 링크 된 목록은 CPU 캐시를 쉽게 죽일 수 있다고 말함으로써 대답 할 수 있습니다 (2). 가능하면 배열이 더 나은 솔루션입니다. 동일한 언어가 다른 언어에 적용되는지에 대한 경험이 없지만 동일한 문제가 발생한다고 상상하기 쉽습니다.


@Andrew : 구조는 어떻습니까. 그들은 캐시 효율적입니까? 캐시 효율을 높이기 위해 크기 제한이 있습니까?
goldenmean

구조체는 단일 메모리 블록이므로 캐시 크기를 초과하지 않는 한 영향을 미치지 않습니다. 캐시 적중을 볼 수있는 구조체 (또는 클래스) 컬렉션이있는 경우에만 컬렉션을 구성하는 방법에 따라 다릅니다. 배열은 객체를 서로 맞대고 있지만 (양호한) 링크 된 목록은 주소 공간 전체에 링크가있는 객체를 가질 수 있으며 이는 캐시 성능에 좋지 않습니다.
Andrew

큰 목록이 아닌 경우 가장 효과적인 캐시를 종료하지 않고 연결된 목록을 사용하는 방법은 자체 메모리 풀을 만드는 것입니다. 즉, 하나의 큰 배열을 할당하는 것입니다. 메모리에 완전히 다른 위치에 할당 될 수있는 작은 링크 된 목록 멤버 각각에 대해 'malloc'ing (또는 C ++에서'new'ing) 메모리 대신 낭비되는 공간을 관리하면 메모리 풀에서 메모리를 제공합니다. 논리적으로 목록의 멤버를 닫을 확률을 높이면 캐시에있게됩니다.
Liran Orevi 2009

물론 std :: list <> et al. 사용자 정의 메모리 블록을 사용하십시오. 내가 어리석은 어렸을 때 나는 절대 그 길을 가고 싶었지만 요즘에는 너무 많은 것들을 다루기 힘들어합니다.
Andrew


4

캐시는 "캐시 라인"으로 정렬되며이 크기의 청크에서 (실제) 메모리를 읽고 씁니다.

따라서 단일 캐시 라인에 포함 된 데이터 구조가 더 효율적입니다.

마찬가지로 연속 메모리 블록에 액세스하는 알고리즘은 임의 순서로 메모리를 뛰어 넘는 알고리즘보다 효율적입니다.

불행히도 캐시 라인 크기는 프로세서마다 크게 다르므로 한 프로세서에서 최적의 데이터 구조가 다른 프로세서에서 효율적일 것이라고 보장 할 방법이 없습니다.


반드시 그런 것은 아닙니다. 허위 공유에주의하십시오. 때로는 데이터를 다른 캐시 라인으로 분할해야합니다. 캐시의 효율성은 항상 사용 방법에 달려 있습니다.
DAG

4

캐시를 효율적으로 캐시하고 코드를 캐시하는 방법과 다른 대부분의 질문은 일반적으로 프로그램을 최적화하는 방법을 묻는 것입니다. 캐시는 성능에 큰 영향을 미치기 때문에 모든 최적화 된 프로그램이 캐시입니다. 효과적인 캐시 친화적.

최적화에 대해 읽으십시오.이 사이트에는 좋은 답변이 있습니다. 책의 관점에서, 나는 캐시의 올바른 사용법에 관한 좋은 글을 가지고 있는 Computer Systems : Programmer 's Perspective 를 추천 합니다.

(btw-캐시 미스가 나쁘면 나빠집니다-프로그램이 하드 드라이브에서 페이징 하는 경우 ...)


4

데이터 구조 선택, 액세스 패턴 등과 같은 일반적인 조언에 대한 많은 답변이 있습니다. 여기서는 활성 캐시 관리를 사용하는 소프트웨어 파이프 라인 이라는 다른 코드 디자인 패턴을 추가하고 싶습니다 .

이 아이디어는 다른 파이프 라인 기술 (예 : CPU 명령 파이프 라인)에서 차용됩니다.

이 유형의 패턴은 다음과 같은 절차에 가장 적합합니다.

  1. 실행 시간이 RAM 액세스 시간 (~ 60-70ns)과 거의 비슷한 합리적인 여러 하위 단계 인 S [1], S [2], S [3]으로 분류 될 수 있습니다.
  2. 일련의 입력을 받고 결과를 얻기 위해 위에서 언급 한 여러 단계를 수행합니다.

하위 절차가 하나만있는 간단한 사례를 살펴 보겠습니다. 일반적으로 코드는 다음과 같습니다.

def proc(input):
    return sub-step(input))

더 나은 성능을 얻으려면 여러 입력을 일괄 적으로 함수에 전달하여 함수 호출 오버 헤드를 상각하고 코드 캐시 위치를 증가시킬 수 있습니다.

def batch_proc(inputs):
    results = []
    for i in inputs:
        // avoids code cache miss, but still suffer data(inputs) miss
        results.append(sub-step(i))
    return res

그러나 앞에서 언급했듯이 단계 실행이 RAM 액세스 시간과 대략 동일하면 코드를 다음과 같이 더 향상시킬 수 있습니다.

def batch_pipelined_proc(inputs):
    for i in range(0, len(inputs)-1):
        prefetch(inputs[i+1])
        # work on current item while [i+1] is flying back from RAM
        results.append(sub-step(inputs[i-1]))

    results.append(sub-step(inputs[-1]))

실행 흐름은 다음과 같습니다.

  1. prefetch (1) 은 CPU에 입력 [1]을 캐시로 프리 페치하도록 요청합니다. 여기서 프리 페치 명령어는 P주기 자체를 취하여 리턴하며 백그라운드 입력 [1]은 R주기 후에 캐시에 도착합니다.
  2. works_on (0) 콜드 미스 0에서 작동하고 M이 걸립니다.
  3. 프리 페치 (2) 다른 페치를 발행
  4. works_on (1) P + R <= M 인 경우 입력 [1]이이 단계 이전에 이미 캐시에 있어야하므로 데이터 캐시 누락을 피하십시오
  5. works_on (2) ...

더 많은 단계가 필요할 수 있으며 단계의 타이밍과 메모리 액세스 대기 시간이 일치하는 한 코드 / 데이터 캐시 누락이 거의없는 다단계 파이프 라인을 설계 할 수 있습니다. 그러나이 프로세스는 올바른 단계 그룹화 및 프리 페치 시간을 찾기 위해 많은 실험으로 조정해야합니다. 필요한 노력으로 인해 고성능 데이터 / 패킷 스트림 처리에 더 많이 채택되고 있습니다. DPDK QoS Enqueue 파이프 라인 디자인에서 좋은 프로덕션 코드 예제를 찾을 수 있습니다. http://dpdk.org/doc/guides/prog_guide/qos_framework.html 21.2.4.3 장. 인큐 파이프 라인.

더 많은 정보를 찾을 수 있습니다 :

https://software.intel.com/en-us/articles/memory-management-for-optimal-performance-on-intel-xeon-phi-coprocessor-alignment-and

http://infolab.stanford.edu/~ullman/dragon/w06/lectures/cs243-lec13-wei.pdf


1

최소한의 크기로 프로그램을 작성하십시오. 그렇기 때문에 GCC에 -O3 최적화를 사용하는 것이 항상 좋은 생각은 아닙니다. 더 큰 크기를 차지합니다. 종종 -Os는 -O2만큼 좋습니다. 그것은 모두 사용되는 프로세서에 달려 있습니다. YMMV.

한 번에 작은 데이터 덩어리로 작업하십시오. 그렇기 때문에 데이터 세트가 크면 덜 효율적인 정렬 알고리즘이 퀵 정렬보다 빠르게 실행될 수 있습니다. 더 큰 데이터 세트를 더 작은 데이터 세트로 나누는 방법을 찾으십시오. 다른 사람들은 이것을 제안했습니다.

시간 / 공간 공간 명령을보다 잘 활용하기 위해 코드가 어셈블리로 변환되는 방법을 연구 할 수 있습니다. 예를 들면 다음과 같습니다.

for(i = 0; i < MAX; ++i)
for(i = MAX; i > 0; --i)

두 루프는 단순히 배열을 통해 구문 분석하는 경우에도 다른 코드를 생성합니다. 어쨌든 귀하의 질문은 아키텍처에 따라 다릅니다. 따라서 캐시 사용을 엄격하게 제어하는 ​​유일한 방법은 하드웨어 작동 방식을 이해하고 코드를 최적화하는 것입니다.


흥미로운 점. 미리보기 캐시는 루프 / 패스 스루 메모리 방향에 따라 가정합니까?
앤드류

1
추론 적 데이터 캐시를 설계하는 방법에는 여러 가지가 있습니다. 보폭 기반은 데이터 액세스의 '거리'와 '방향'을 측정합니다. 컨텐츠 기반은 포인터 체인을 쫓습니다. 그것들을 디자인하는 다른 방법이 있습니다.
sybreon 2009

1

구조와 필드를 정렬하는 것 외에도 힙이 할당 된 경우 구조가 정렬 된 할당을 지원하는 할당자를 사용할 수 있습니다. _aligned_malloc (sizeof (DATA), SYSTEM_CACHE_LINE_SIZE)와 같은; 그렇지 않으면 임의의 허위 공유가있을 수 있습니다. Windows에서 기본 힙의 정렬은 16 바이트입니다.

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