지역성 원리 (Principle of Locality): 잘 작성된 컴퓨터 프로그램들이 보이는 경향으로, 최근에 참조된 데이터 항목들 근처의 데이터나 최근에 참조된 데이터 자체를 다시 참조하는 성향
하드웨어 레벨 → 캐시 메모리 설계
운영체제 레벨 → 가상 메모리, 파일 시스템 캐싱
응용프로그램 레벨 → 웹 브라우저 캐싱, 서버 캐싱
하드웨어:
운영체제:
응용 프로그램:
한 번 참조된 메모리 위치가 가까운 미래에 다시 여러 번 참조될 가능성이 높은 특성
int sum = 0;
for (i = 0; i < N; i++) {
sum += array[i]; // sum 변수가 시간적 지역성을 보임
}
한 메모리 위치가 참조되면, 그 근처의 메모리 위치들이 가까운 미래에 참조될 가능성이 높은 특성
int array[N];
for (i = 0; i < N; i++) {
sum += array[i]; // 배열 원소들이 공간적 지역성을 보임
}
| 특성 | 시간적 지역성 | 공간적 지역성 |
|---|---|---|
| 핵심 개념 | 같은 위치 재참조 | 인근 위치 참조 |
| 메모리 패턴 | 동일 주소 반복 | 연속 주소 순차 |
| 대표 사례 | 변수 재사용 | 배열 순회 |
| 캐시 효과 | 히트율 향상 | 프리페칭 효과 |
int sumvec(int v[N]) {
int i, sum = 0;
for (i = 0; i < N; i++)
sum += v[i];
return sum;
}
주소: 0 4 8 12 16 20 24 28
내용: v0 v1 v2 v3 v4 v5 v6 v7
접근순서: 1 2 3 4 5 6 7 8
sum 변수:
배열 v:
결론: 전체적으로 좋은 지역성을 가짐
// 좋은 공간적 지역성
for (i = 0; i < N; i++)
sum += array[i]; // stride-1 패턴
// 나쁜 공간적 지역성
for (i = 0; i < N; i += k)
sum += array[i]; // stride-k 패턴 (k > 1)
int sumarrayrows(int a[M][N]) {
int i, j, sum = 0;
for (i = 0; i < M; i++) // 외부 루프: 행
for (j = 0; j < N; j++) // 내부 루프: 열
sum += a[i][j];
return sum;
}
메모리 배치와 접근 패턴:
메모리 주소: 0 4 8 12 16 20
배열 내용: a00 a01 a02 a10 a11 a12
접근 순서: 1 2 3 4 5 6
지역성 분석:
int sumarraycols(int a[M][N]) {
int i, j, sum = 0;
for (j = 0; j < N; j++) // 외부 루프: 열
for (i = 0; i < M; i++) // 내부 루프: 행
sum += a[i][j];
return sum;
}
메모리 배치와 접근 패턴:
메모리 주소: 0 4 8 12 16 20
배열 내용: a00 a01 a02 a10 a11 a12
접근 순서: 1 3 5 2 4 6
지역성 분석:
// M=1000, N=1000인 2차원 배열의 경우
// 행 우선 순회: 캐시 친화적 → 빠른 실행
// 열 우선 순회: 캐시 비친화적 → 느린 실행
// 성능 차이: 수배에서 수십 배까지 가능
// 2차원 배열을 열 우선으로 처리
for (j = 0; j < N; j++)
for (i = 0; i < M; i++)
result += matrix[i][j];
// 2차원 배열을 행 우선으로 처리
for (i = 0; i < M; i++)
for (j = 0; j < N; j++)
result += matrix[i][j];
간단한 루프 순서 변경만으로 극적인 성능 향상 가능!
for (i = 0; i < N; i++)
sum += v[i];
공간적 지역성:
시간적 지역성:
// 좋은 지역성: 순차적 실행
if (condition) {
statement1;
statement2;
}
// 나쁜 지역성: 점프가 많은 코드
if (cond1) goto label1;
if (cond2) goto label2;
// ...
□ 변수들이 반복적으로 사용되는가? (시간적 지역성)
□ 배열 접근이 순차적인가? (공간적 지역성)
□ 루프가 메모리를 건너뛰며 접근하는가? (Stride 패턴)
□ 다차원 배열 접근 순서가 저장 순서와 일치하는가?
□ 함수 호출이 지나치게 많은가? (명령어 지역성)
// 표준 3중 루프: 캐시 미스 많음
for (i = 0; i < n; i++)
for (j = 0; j < n; j++)
for (k = 0; k < n; k++)
C[i][j] += A[i][k] * B[k][j]; // B[k][j] 접근이 stride-n
// 블록 단위 처리: 캐시 지역성 향상
for (ii = 0; ii < n; ii += BLOCK_SIZE)
for (jj = 0; jj < n; jj += BLOCK_SIZE)
for (kk = 0; kk < n; kk += BLOCK_SIZE)
for (i = ii; i < ii + BLOCK_SIZE; i++)
for (j = jj; j < jj + BLOCK_SIZE; j++)
for (k = kk; k < kk + BLOCK_SIZE; k++)
C[i][j] += A[i][k] * B[k][j];
AoS (나쁜 지역성):
struct Point {
float x, y, z;
};
struct Point points[N];
// 모든 x 좌표만 처리하고 싶을 때
for (i = 0; i < N; i++)
sum_x += points[i].x; // x, y, z가 섞여서 저장됨
SoA (좋은 지역성):
struct Points {
float x[N];
float y[N];
float z[N];
};
struct Points points;
// 모든 x 좌표만 처리할 때
for (i = 0; i < N; i++)
sum_x += points.x[i]; // x들만 연속으로 저장됨
// 시간적 지역성 활용
if (cache_contains(url)) {
return cached_content(url); // 캐시 히트
} else {
content = fetch_from_disk(url);
cache_store(url, content); // 향후 재사용을 위해 캐싱
return content;
}
-- 최근 접근한 페이지들을 메모리에 유지
-- 공간적 지역성: 인접한 레코드들을 같은 페이지에 저장
-- 시간적 지역성: 자주 접근하는 페이지들을 캐시에 유지
// Before: 나쁜 지역성
for (j = 0; j < N; j++)
for (i = 0; i < M; i++)
matrix[i][j] += value;
// After: 좋은 지역성
for (i = 0; i < M; i++)
for (j = 0; j < N; j++)
matrix[i][j] += value;
// 큰 배열을 작은 블록으로 나누어 처리
for (ii = 0; ii < M; ii += TILE_SIZE)
for (jj = 0; jj < N; jj += TILE_SIZE)
for (i = ii; i < min(ii + TILE_SIZE, M); i++)
for (j = jj; j < min(jj + TILE_SIZE, N); j++)
process(matrix[i][j]);
// Before: 캐시 라인 낭비
struct BadLayout {
char flag; // 1 byte
int data1; // 4 bytes (3 bytes padding)
char status; // 1 byte
int data2; // 4 bytes (3 bytes padding)
};
// After: 메모리 효율적
struct GoodLayout {
int data1; // 4 bytes
int data2; // 4 bytes
char flag; // 1 byte
char status; // 1 byte (2 bytes padding)
};