아래는 제가 전환하는 것을 제외하고는 거의 동일한 두 개의 프로그램입니다 i
및 j
주변 변수. 둘 다 다른 시간에 실행됩니다. 왜 이런 일이 발생했는지 설명해 주시겠습니까?
버전 1
#include <stdio.h>
#include <stdlib.h>
main () {
int i,j;
static int x[4000][4000];
for (i = 0; i < 4000; i++) {
for (j = 0; j < 4000; j++) {
x[j][i] = i + j; }
}
}
버전 2
#include <stdio.h>
#include <stdlib.h>
main () {
int i,j;
static int x[4000][4000];
for (j = 0; j < 4000; j++) {
for (i = 0; i < 4000; i++) {
x[j][i] = i + j; }
}
}
답변
다른 사람들이 말했듯이 문제는 배열의 메모리 위치에 대한 저장소 x[i][j]
입니다. 이유는 다음과 같습니다.
2 차원 배열이 있지만 컴퓨터의 메모리는 본질적으로 1 차원입니다. 따라서 배열을 다음과 같이 상상하십시오.
0,0 | 0,1 | 0,2 | 0,3
----+-----+-----+----
1,0 | 1,1 | 1,2 | 1,3
----+-----+-----+----
2,0 | 2,1 | 2,2 | 2,3
컴퓨터는이를 한 줄로 메모리에 저장합니다.
0,0 | 0,1 | 0,2 | 0,3 | 1,0 | 1,1 | 1,2 | 1,3 | 2,0 | 2,1 | 2,2 | 2,3
두 번째 예에서는 두 번째 숫자를 먼저 반복하여 배열에 액세스합니다.
x[0][0]
x[0][1]
x[0][2]
x[0][3]
x[1][0] etc...
당신이 순서대로 그들을 때리는 것을 의미합니다. 이제 첫 번째 버전을보십시오. 당신은하고 있습니다 :
x[0][0]
x[1][0]
x[2][0]
x[0][1]
x[1][1] etc...
C가 메모리에 2 차원 배열을 배치하는 방식으로 인해 모든 곳을 뛰어 넘도록 요구하고 있습니다. 그러나 이제 키커에게 : 왜 이것이 문제가 되는가? 모든 메모리 액세스가 동일합니까?
아니요 : 캐시 때문입니다. 메모리의 데이터는 일반적으로 64 바이트 인 작은 청크 ( ‘캐시 라인’이라고 함)로 CPU로 가져옵니다. 4 바이트 정수가 있다면 깔끔한 작은 번들로 16 개의 연속 정수를 얻는다는 의미입니다. 실제로 이러한 메모리 덩어리를 가져 오는 것은 상당히 느립니다. CPU는 단일 캐시 라인을로드하는 데 걸리는 시간에 많은 작업을 수행 할 수 있습니다.
이제 액세스 순서를 다시 살펴보십시오. 두 번째 예는 (1) 16 개 덩어리 청크를 잡고 (2) 모두 수정하고, (3) 4000 * 4000 / 16 회 반복합니다. 훌륭하고 빠르며 CPU는 항상 작동해야합니다.
첫 번째 예는 (1) 16 개의 덩어리 청크를 잡고, (2) 그중 하나만 수정하고, (3) 4000 * 4000 번 반복합니다. 메모리에서 “페치”횟수의 16 배가 필요합니다. 실제로 CPU는 해당 메모리가 표시되기를 기다리는 데 시간을 소비해야하며, 앉아있는 동안 귀중한 시간을 낭비하고 있습니다.
중요 사항:
이제 답을 얻었으니 여기에 흥미로운 메모가 있습니다. 두 번째 예제가 빠른 이유는 없습니다. 예를 들어, 포트란에서 첫 번째 예는 빠르고 두 번째 예는 느립니다. C와 같은 개념적인 “행”으로 확장하는 대신, 포트란은 “열”로 확장되기 때문입니다.
0,0 | 1,0 | 2,0 | 0,1 | 1,1 | 2,1 | 0,2 | 1,2 | 2,2 | 0,3 | 1,3 | 2,3
C의 레이아웃을 ‘행 전공’이라고하고 포트란을 ‘열 전공’이라고합니다. 보다시피 프로그래밍 언어가 행 전공인지 열 전공인지 아는 것이 매우 중요합니다! 자세한 내용은 다음 링크를 참조하십시오 : http://en.wikipedia.org/wiki/Row-major_order
답변
어셈블리와 관련이 없습니다. 이것은 캐시 누락 으로 인한 것 입니다.
C 다차원 배열은 마지막 차원을 가장 빠르게 저장합니다. 따라서 첫 번째 버전은 모든 반복에서 캐시를 놓치지 만 두 번째 버전은 그렇지 않습니다. 따라서 두 번째 버전은 훨씬 빠릅니다.
http://en.wikipedia.org/wiki/Loop_interchange 도 참조하십시오 .
답변
버전 2는 버전 1보다 컴퓨터의 캐시를 더 잘 사용하기 때문에 훨씬 더 빠르게 실행됩니다. 생각하면 배열은 메모리의 연속적인 영역 일뿐입니다. 배열의 요소를 요청하면 OS에서 해당 요소가 포함 된 메모리 페이지를 캐시로 가져옵니다. 그러나 다음 몇 개의 요소도 연속되어 있기 때문에 해당 페이지에 있으므로 다음 액세스는 이미 캐시에 있습니다! 이것이 버전 2가 속도를 높이기 위해하는 일입니다.
반면, 버전 1은 행이 아닌 열 방식으로 요소에 액세스합니다. 이러한 종류의 액세스는 메모리 수준에서 연속적이지 않으므로 프로그램은 OS 캐싱을 많이 이용할 수 없습니다.
답변
그 이유는 캐시 로컬 데이터 액세스입니다. 두 번째 프로그램에서는 캐싱 및 프리 페치의 이점이있는 메모리를 통해 선형으로 스캔합니다. 첫 번째 프로그램의 메모리 사용 패턴이 훨씬 넓어 지므로 캐시 동작이 악화됩니다.
답변
캐시 적중률에 대한 다른 훌륭한 답변 외에도 가능한 최적화 차이가 있습니다. 두 번째 루프는 컴파일러에 의해 다음과 같은 것으로 최적화 될 수 있습니다.
for (j=0; j<4000; j++) {
int *p = x[j];
for (i=0; i<4000; i++) {
*p++ = i+j;
}
}
매번 4000으로 포인터 “p”를 증가시켜야하기 때문에 이것은 첫 번째 루프에서는 가능성이 적습니다.
편집 : p++
심지어 *p++ = ..
대부분의 CPU에서 단일 CPU 명령으로 컴파일 할 수도 있습니다. *p = ..; p += 4000
최적화 할 수없는 이점이 없습니다. 컴파일러는 내부 배열의 크기를 알고 사용해야하기 때문에 더 어렵습니다. 그리고 보통 코드의 내부 루프에서 종종 발생하지 않습니다 (마지막 인덱스가 루프에서 일정하게 유지되고 두 번째에서 마지막 인덱스가 단계 화되는 다차원 배열의 경우에만 발생). 최적화가 우선 순위가 아닙니다. .
답변
이 선의 범인 :
x[j][i]=i+j;
두 번째 버전은 연속 메모리를 사용하므로 훨씬 빠릅니다.
나는 함께 노력했다
x[50000][50000];
실행 시간은 version1의 경우 13 초이며 version2의 경우 0.6 초입니다.
답변
나는 일반적인 대답을하려고합니다.
C에서 i[y][x]
속기 때문에 *(i + y*array_width + x)
(고상한 시도 int P[3]; 0[P] = 0xBEEF;
).
반복 y
할 때 크기의 덩어리를 반복합니다 array_width * sizeof(array_element)
. 내부 루프 array_width * array_height
에 해당 항목 이 있으면 해당 청크에 대해 반복 됩니다 .
순서를 뒤집 으면 array_height
청크 반복 만 발생하고 청크 반복 사이에는 array_width
반복 만 수행됩니다 sizeof(array_element)
.
실제로 오래된 x86-CPU에서는 그다지 중요하지 않았지만 오늘날의 x86은 많은 프리 페치 및 데이터 캐싱을 수행합니다. 느린 반복 순서로 많은 캐시 미스가 발생할 수 있습니다.