Loop Fusion & Loop Fission

다음 두 코드는 좀 복잡해 보이지만 하는 일이 똑같다. 어느 코드가 더 빠를까? (병렬 행렬 곱셈 코드 중 일부분인데, 행렬을 격자 모양으로 자른 뒤, 그것을 각각 곱하고 결과를 취합하는 코드이다.)

#pragma omp for 
for (i = 0; i < M/B; ++i) {
for (j = 0; j < M/B; ++j) {
for (k = 0; k < M/B; ++k) {
for (x = 0; x < B; ++x) {
for (y = 0; y < B; ++y) {
for (z = 0; z < B; ++z) {
tC[i*B*M+j*B*B+x*B+y] += tA[i*B*M+k*B*B+x*B+z] * tB[k*B*M+j*B*B+z*B+y];
}
}
}
}
}
}

#pragma omp for
for (i = 0; i < M/B; ++i)
for (j = 0; j < M/B; ++j)
for (x = 0; x < B; ++x)
for (y = 0; y < B; ++y)
par_tiled_output[i*B*M+j*B+x*M+y] = tC[i*B*M+j*B*B+x*B+y];
#pragma omp for 
for (i = 0; i < M/B; ++i) {
for (j = 0; j < M/B; ++j) {
for (k = 0; k < M/B; ++k) {
for (x = 0; x < B; ++x) {
for (y = 0; y < B; ++y) {
for (z = 0; z < B; ++z) {
tC[i*B*M+j*B*B+x*B+y] += tA[i*B*M+k*B*B+x*B+z] * tB[k*B*M+j*B*B+z*B+y];
}
par_tiled_output[i*B*M+j*B+x*M+y] = tC[i*B*M+j*B*B+x*B+y];
}
}
}
}
}

썰렁한 답변이지만 "그 때 그 때 달라요"가 정답이다. 그러나 대부분의 경우 후자보다 전자의 코드가 더 빠르게 돌아갈 확률이 크다고 답할 수 있다. 특히 tC, tA, tB와 같은 배열의 크기가 제법 클 경우에는 전자가 더 빨리 돌아간다.

그 이유는 쉽게 예상하겠지만, 전자의 코드가 데이터 접근의 지역성(locality)를 더 많이 활용하므로 캐쉬 미스가 더 줄고, 결과적으로 더 빠르게 작동할 수 있는 것이다. 캐쉬 미스를 줄여, 복잡한 for 루프로 인한 각종 오버헤드를 뛰어넘는 것이다. 물론 분기문이 들어가면 골치아파지지만 규칙성이 강한 for 루프는 비교적 큰 어려움 없이 CPU가 처리할 수 있다.

실제로 위 두 코드를 Xeon 쿼드 코어 프로세서에서 돌려보면 전자의 코드가 5~8% 정도 빠른 성능을 일관되게 보여준다. 만약 코드에서 사용된 배열의 크기가 크다면 더 큰 성능 차이를 보일 수도 있다.


이러한 기법을 흔히 Loop Fission, 루프 분할(?) 라고 부른다.

// Original loop
int i, a[100], b[100];
for (i = 0; i < 100; i++) {
a[i] = 1;
b[i] = 2;
}
// After loop fission
int i, a[100], b[100];
for (i = 0; i < 100; i++) {
a[i] = 1;
}
for (i = 0; i < 100; i++) {
b[i] = 2;
}

이렇게 데이터 참조하는 부분을 각기 다른 루프에 나눠 캐쉬 사용성을 극대화 하는 것이다. 그리고 이 반대로 하나의 루프로 합치는 것을 Loop Fusion이라고 부르기도 한다. 위에서 예로든 행렬 곱셈의 경우에는 오히려 루프를 합치는 것이 나쁜 경우이다.

요즘 L2 캐쉬가 6MB 씩이나 되고 (물론 듀얼코어가 합쳐서 쓰는 용량이기는 하지만) 속도가 많이 빨라져서 캐쉬 효율성까지 고려한 프로그래밍을 과연 데스크탑, 서버에서 짜야하는지 의문이 들지도 모른다. 그러나 여전히 이런 저수준의 최적화는 매우 효과적이다. 자료구조의 형태를 보다 캐쉬 후렌들리(...) 하게 바꾸면 지금의 아주 빠른 최신 듀얼 코어 프로세서에서도 여전히 큰 성능 향상을 기대할 수 있다.

by object | 2008/02/03 17:54 | 컴퓨터 | 트랙백 | 덧글(5)
트랙백 주소 : http://minjang.egloos.com/tb/1732780
☞ 내 이글루에 이 글과 관련된 글 쓰기 (트랙백 보내기) [도움말]
Commented by AnonymousY at 2008/02/03 23:11
학부시절 숙제로 locality를 이용해 성능 향상을 보여주는 비교 예제 코드를 짜오는게 있었습니다만...캐시가 원체 크고 빨라서 64K 정도의 샘플 데이터로는 도저히 속도가 빨라지지 않고 오히려 예상과는 반대로 나타나서 고생한 기억이 있습니다. 일부러 강제로 메모리를 읽어오게 해서 해결했던 기억이...; 정말 사람이 h/w 발전 속도를 따라가질 못하네요;
Commented by 오스카 at 2008/02/04 02:14
후렌들리에서 뒤집어졌;;;
Commented by object at 2008/02/04 03:38
그래서 옛날에 만들어진 벤치마크 (e.g. SPLASH2) 같은 것은 워낙 memory footprint가 작아서 지금의 컴퓨터에서는 캐쉬에 몽땅 코드나 데이터가 다 올라오죠. 그런데 행렬 곱셈 예를 들어 1024x1024는 지금의 Core2Duo에서도 엄청나게 캐쉬에 민감합니다.
Commented by felucca at 2008/02/06 01:06
남자라면 4096x4096 정도는....
Commented by Darkttd at 2010/05/08 10:22
안녕하세요, 용묵 님의 홈페이지에서 링크를 타고 넘어왔습니다(..)

최근에 숙제로 쓰레드를 이용한 행렬곱을 구하는 숙제가 있었습니다만
처음에는 단순히 해가지고 갔는데 수업이 끝나고 교수님이 루프순서를 바꿔보라는 자율과제를 추가로 내주셨습니다. (그냥 집에가서 해보고 보고는 안해도 된다.. 수준)

그래서 돌아와서 루프순서를 바꿔보다가 깜짝놀랐습니다.

float행렬 A[1024][1024] 와 float행렬 B[1024][1024] 의 곱을 R[1024][1024] 에 기록할 때
for(int i =0; i < ArSize; i++)
for(int j = 0; j < ArSize; j++)
for(int k = 0; k < ArSize; k++)
R[i][j] += A[i][k] * B[k][j];
보다
for(int i =0; i < ArSize; i++)
for(int k = 0; k < ArSize; k++)
for(int j = 0; j < ArSize; j++)
R[i][j] += A[i][k] * B[k][j];
가 훨씬 빠르더군요.
제가 깜짝놀란건 약 7.5배 정도 속도차이가 났다는거 -_-
(린필드 i5 750 2.67GHz, 아시다시피 쿼드코어; Windows 7 Ultimate K x64기준. 컴파일은 32bit로. 캐시 사이즈는 L2 256KB x4, L3 8MB)
(1쓰레드기준 전자가 9초, 후자가 1.2초
16쓰레드기준 전자가 2.3초, 후자가 0.3초)

환경이 달라지면 속도비가 달리지기는 하겠지만 이렇게까지 캐쉬의 민감도에 인한 속도차는 처음 겪어봅니다..

:         :

:

비공개 덧글

<< 이전 페이지 다음 페이지 >>





by 김민장 2008 이글루스 TOP 100
최근 등록된 덧글
개발자 입장에서의 수많은 ..
by Jiyoon at 02/04
저도 아들 돌잔치때 돌잡이 ..
by 박상욱 at 01/18
미국 대학원 원서 작성중에 p..
by 태클사이야 at 01/13
TO: 박PD 로그인 하지 않아..
by 박응용 at 01/10
http://gigglehd.com/zbx..
by dhunter at 12/28
우와.. 좋네요. 태반이 ..
by 윤광배 at 12/17
항상 좋은 글 잘 보고 있습니..
by y2k at 11/23
글이 좋아서 제 블로그에 담..
by 쏭섭 at 11/23
최근 등록된 트랙백
조엘 스폴스키의 강연 (Sta..
by 인덕원칸타타
[Redis] sds.c를 분..
by 조급하지말고 천천히
메뉴릿
이글루 파인더

website counter

Add to Google

rss

skin by 이글루스