홈으로 돌아가기

배열과 캐시: O(1) 접근 최적화

배열 챕터는 캐시 지역성이 실제 O(1) 접근 속도를 어떻게 결정하는지 보여줍니다. 스트라이드, 다차원 배열, 행렬 곱셈 및 AoS/SoA 분석과 최대 10× 가속 벤치마크.

배열이 캐시를 죽인다: 10배 속도 향상 방법
Advertisement 728x90

캐시 미스를 최소화하기 위한 배열 최적화

배열 요소에 대한 순차 접근은 캐시 메모리의 동작 방식 때문에 무작위 접근보다 7–10배 빠릅니다. 단일 64바이트 캐시 라인은 16개의 순차적인 int를 로드하며, 적절한 순회 패턴을 사용하면 미스를 최소화합니다. perf stat으로 프로파일링하면 문제가 드러납니다: 1M 명령어당 450k 캐시 미스는 간단한 패킷 처리 작업에서도 배열 사용이 비최적화됨을 나타냅니다.

접근 스트라이드의 성능 영향

접근 스트라이드는 캐시 라인 로딩 효율성을 결정합니다. 스트라이드 1일 때 모든 64바이트가 완전히 활용되며, 프리페처가 패턴을 인식해 가져오기를 가속화합니다.

int sum = 0;
for (int i = 0; i < 8; i++) {
    sum += array[i];
}

이런 순서는 8개 요소에 107 사이클(요소당 13.4 사이클)이 걸립니다. 인덱스를 통한 무작위 접근은 비용을 800 사이클(요소당 100 사이클)로 증가시킵니다.

Google AdInline article slot

1M 요소 배열 벤치마크:

  • 스트라이드 1: 1.2 ms (100% 라인 활용도)
  • 스트라이드 2: 1.3 ms (50% 활용도)
  • 스트라이드 4: 1.5 ms
  • 스트라이드 8: 2.1 ms
  • 스트라이드 16: 3.8 ms (6.25% 활용도)
  • 스트라이드 64: 8.5 ms

권장 사항: 수용 가능한 성능을 위해 스트라이드 ≤8 요소. lmbench lat_mem_rd 도구가 확인해줍니다: 작은 스트라이드(128–512 바이트)는 데이터를 L1에 유지(3–4 ns), 큰 스트라이드(64 KB)는 DRAM으로 밀어냄(100+ ns).

다차원 배열: 순회 순서가 핵심

C는 행 우선 순서를 사용합니다: 행 내 요소는 메모리에서 연속적입니다. 행 단위 순회는 순차 접근을 보장하지만, 열 단위 순회는 16+ 바이트 스트라이드를 유발합니다.

Google AdInline article slot
int matrix[4][4] = {
    {0, 1, 2, 3},
    {4, 5, 6, 7},
    {8, 9, 10, 11},
    {12, 13, 14, 15}
};

1024×1024 행렬에서 행 순회는 12 ms, 열 순회는 45 ms(3.75배 느림)가 걸립니다.

행렬 곱셈 최적화

나이브 ijk 순서:

for (int i = 0; i < N; i++) {
    for (int j = 0; j < N; j++) {
        for (int k = 0; k < N; k++) {
            C[i][j] += A[i][k] * B[k][j];
        }
    }
}

문제: B[k][j]가 열 단위로 읽힘(스트라이드 N=1024, 4096 바이트). ikj 순서로 수정:

Google AdInline article slot
for (int i = 0; i < N; i++) {
    for (int k = 0; k < N; k++) {
        int r = A[i][k];
        for (int j = 0; j < N; j++) {
            C[i][j] += r * B[k][j];
        }
    }
}

512×512 결과: ijk — 2450 ms, ikj — 680 ms (3.6배 속도 향상).

큰 행렬의 경우 — BLOCK_SIZE=64 타일링:

for (int ii = 0; ii < N; ii += BLOCK_SIZE) {
    for (int jj = 0; jj < N; jj += BLOCK_SIZE) {
        for (int kk = 0; kk < N; kk += BLOCK_SIZE) {
            for (int i = ii; i < min(ii + BLOCK_SIZE, N); i++) {
                for (int k = kk; k < min(kk + BLOCK_SIZE, N); k++) {
                    int r = A[i][k];
                    for (int j = jj; j < min(jj + BLOCK_SIZE, N); j++) {
                        C[i][j] += r * B[k][j];
                    }
                }
            }
        }
    }
}

블록이 L1에 맞고 데이터 재사용이 이루어집니다. 1024×1024: 1800 ms (나이브보다 10배 빠름).

AoS vs SoA: 데이터 조직

구조체의 배열(AoS)은 입자 필드를 그룹화하지만, 캐시 라인에 미사용 데이터가 포함되어 활용도가 37.5%입니다.

typedef struct {
    float x, y, z;
    float vx, vy, vz;
    float mass;
    int id;
} particle_t;

배열의 구조체(SoA)는 타입별로 분리합니다:

typedef struct {
    float x[1000], y[1000], z[1000];
    float vx[1000], vy[1000], vz[1000];
    float mass[1000];
    int id[1000];
} particles_t;

SoA의 위치 업데이트는 캐시 라인을 100% 활용합니다. 1M 입자, 1000 반복 벤치마크: AoS — 2850 ms, SoA — 1200 ms (2.4배 속도 향상).

주요 요점

  • 순차 배열 접근은 무작위 접근 대비 캐시 미스를 7배 이상 최소화
  • 접근 스트라이드 ≤8 요소는 >90% 캐시 라인 활용을 보장
  • C의 행 우선 순서는 행 순회를 요구; 열 접근은 3–4배 느려짐
  • 행렬 연산의 루프 순서 변경으로 3–4배 속도 향상
  • SIMD 처리와 필드 부분 빈번 접근에서 SoA가 AoS를 능가

— Editorial Team

Advertisement 728x90

다음 읽기