캐시 최적화와 메모리 관리 기법은 실시간 시스템에서 성능을 극대화하기 위한 중요한 요소이다. Preempt RT 환경에서 캐시와 메모리의 효율적 사용은 실시간 응답성을 유지하면서도 높은 처리 성능을 보장하는 데 필수적이다. 이 장에서는 캐시의 동작 원리와 최적화 방법, 메모리 관리 기법에 대해 다루겠다.

캐시 메모리의 기본 개념

캐시 메모리는 CPU와 메인 메모리 간의 속도 차이를 줄이기 위해 사용되는 고속 메모리이다. 캐시는 자주 사용되는 데이터를 메인 메모리에서 복사하여 저장하므로 CPU가 메모리에 접근할 때 더 빠른 속도로 데이터를 읽고 쓸 수 있다. 캐시 최적화를 이해하기 위해서는 다음의 기본 개념들을 이해해야 한다.

캐시 친화적 코딩 기법

캐시 최적화를 위해서는 캐시 히트를 최대화하고 캐시 미스를 최소화하는 코딩 기법이 필요하다. 이를 위해 다음과 같은 전략을 사용할 수 있다.

데이터 지역성(Locality of Reference)

데이터 지역성은 캐시 메모리의 효율성을 높이는 핵심 개념이다. 이는 시간적 지역성(Temporal Locality)과 공간적 지역성(Spatial Locality)으로 나눌 수 있다.

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

위 코드에서 array의 요소들은 연속된 메모리 공간에 존재하기 때문에 공간적 지역성이 높다. 따라서 이 코드에서는 캐시 히트가 많이 발생하여 성능이 향상된다.

캐시 미스 최소화를 위한 기법

캐시 미스는 시스템의 성능을 저하시키는 주요 요인 중 하나이다. 캐시 미스를 줄이기 위한 몇 가지 기법은 다음과 같다.

캐시 차원 관리(Cache Blocking)

캐시 차원 관리 기법은 다차원 배열을 사용하는 계산에서 캐시의 활용도를 극대화하는 기법이다. 예를 들어, 행렬 곱셈에서 큰 행렬을 작은 블록으로 나누어 각각의 블록을 처리함으로써 캐시 히트를 극대화할 수 있다.

예를 들어, 두 행렬 \mathbf{A}\mathbf{B}의 곱셈 \mathbf{C} = \mathbf{A} \times \mathbf{B}에서 블록 기반의 접근 방식은 다음과 같은 형태로 구현할 수 있다.

C_{i,j} = \sum_{k=1}^{N} A_{i,k} \times B_{k,j}

이 기본 형태의 곱셈은 캐시 미스가 많이 발생할 수 있다. 이를 방지하기 위해 블록 기반 접근을 사용하면 다음과 같이 코드가 변경될 수 있다.

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

여기서 B는 블록 크기이다. 이 코드에서는 메모리 접근이 작은 블록 내에서만 이루어지므로 캐시 미스를 줄일 수 있다.

데이터 패딩(Data Padding)

데이터 패딩은 캐시 라인간의 충돌을 피하기 위해 데이터 사이에 불필요한 공간을 추가하는 방법이다. 캐시 라인 충돌이 발생하면 동일한 캐시 라인에 있는 데이터들이 덮어쓰여 성능이 저하될 수 있다.

struct Data {
    int value;
    char padding[60];  // 64바이트 캐시 라인 충돌 방지
};

위의 예에서는 64바이트 캐시 라인 충돌을 방지하기 위해 구조체에 60바이트의 패딩을 추가하였다. 이를 통해 성능이 향상될 수 있다.

메모리 관리 기법

Preempt RT와 같은 실시간 시스템에서는 메모리 관리도 중요한 역할을 한다. 메모리 관리 기법은 시스템의 실시간 성능에 직접적인 영향을 미치며, 적절한 메모리 관리 전략이 없으면 실시간 응답성에 악영향을 미칠 수 있다. 여기에서는 실시간 시스템에서 자주 사용되는 메모리 관리 기법을 다룬다.

페이지 폴트 최소화

페이지 폴트(Page Fault)는 프로세스가 접근하려는 메모리 페이지가 현재 물리 메모리에 없을 때 발생하며, 이는 메모리 접근 시간을 크게 증가시킨다. 실시간 시스템에서는 페이지 폴트로 인해 예측하지 못한 지연이 발생할 수 있으므로 이를 최소화하는 것이 중요하다.

이를 위해 다음과 같은 전략을 사용할 수 있다.

mlockall(MCL_CURRENT | MCL_FUTURE);

이 코드는 현재 프로세스와 미래에 할당될 메모리를 모두 물리 메모리에 고정시킨다. 이로 인해 페이지 폴트가 발생하지 않아 실시간 성능을 보장할 수 있다.

메모리 풀(Memory Pool) 할당

메모리 풀 할당은 실시간 시스템에서 동적 메모리 할당에 따른 비효율성을 줄이기 위해 사용되는 기법이다. 일반적인 동적 메모리 할당은 비결정적 시간 복잡도를 가지므로 실시간 성능에 부정적인 영향을 미칠 수 있다. 메모리 풀은 미리 할당된 메모리 블록을 사용하여 메모리 할당 및 해제 시간을 일정하게 유지할 수 있다.

typedef struct {
    void* blocks[NUM_BLOCKS];
    int free_index;
} MemoryPool;

void* allocate(MemoryPool* pool) {
    if (pool->free_index < NUM_BLOCKS) {
        return pool->blocks[pool->free_index++];
    }
    return NULL;  // 메모리 풀이 가득 찬 경우
}

void deallocate(MemoryPool* pool, void* block) {
    pool->blocks[--pool->free_index] = block;
}

위 예제에서는 MemoryPool 구조체를 통해 메모리 블록을 관리하고 있으며, 이를 통해 동적 메모리 할당/해제에 소요되는 시간을 일정하게 유지할 수 있다.

캐시 일관성 관리(Cache Coherency)

다중 코어 환경에서는 각 코어가 별도의 캐시를 가지고 있으므로, 캐시 일관성 문제(Cache Coherency)가 발생할 수 있다. 이는 실시간 시스템에서 중요한 이슈로, 캐시 일관성 문제를 해결하기 위해 메모리 배치 및 동기화 기법을 적절히 사용해야 한다.

volatile int shared_data;

void update_shared_data(int new_value) {
    __atomic_store_n(&shared_data, new_value, __ATOMIC_SEQ_CST);
}

위 코드에서는 __atomic_store_n 함수를 사용하여 원자적 연산을 통해 shared_data의 일관성을 유지하고 있다.

실시간 가상 메모리 관리

실시간 시스템에서 가상 메모리를 사용하는 경우, 메모리 접근 시간의 예측 가능성을 보장하기 위해 특별한 관리 기법이 필요하다. 가상 메모리는 페이지 폴트와 같은 불확실성을 도입할 수 있으므로, 실시간 응답성을 유지하기 위해 가상 메모리 시스템을 적절히 구성해야 한다.

메모리 정렬 및 정합성

실시간 시스템에서는 메모리 접근의 효율성을 높이기 위해 메모리 정렬(Alignment)과 정합성(Consistency)을 보장하는 것이 중요하다. 이는 특히 SIMD(단일 명령 다중 데이터)와 같은 벡터 연산이나 고성능 프로세서에서 중요한 역할을 한다.

메모리 정렬(Alignment)

메모리 정렬이란 데이터가 메모리의 특정 경계(예: 4바이트, 8바이트)에 맞춰 저장되는 것을 의미한다. 정렬된 메모리는 CPU가 데이터를 읽고 쓰는 속도를 높일 수 있으며, 캐시 라인 활용도를 극대화할 수 있다.

예를 들어, 4바이트 정렬된 정수 배열은 각 요소가 4바이트 경계에 위치하게 되며, 이는 다음과 같이 정의될 수 있다.

int array[10] __attribute__((aligned(4)));

정렬되지 않은 메모리 접근은 추가적인 메모리 접근 사이클을 유발할 수 있으며, 이는 실시간 응답성을 저하시킬 수 있다.

메모리 정합성(Consistency)

메모리 정합성은 다중 스레드나 다중 코어 환경에서 중요한 개념으로, 모든 프로세서가 메모리에 대한 일관된 뷰를 가지도록 보장하는 것을 의미한다. 정합성을 유지하기 위해 다음과 같은 기법을 사용할 수 있다.

__sync_synchronize();

위 함수는 모든 메모리 접근이 완료된 후에 다음 연산이 수행되도록 보장하는 메모리 배리어를 생성한다.

__atomic_fetch_add(&counter, 1, __ATOMIC_SEQ_CST);

위 코드에서는 counter에 원자적으로 1을 더하는 연산을 수행한다. 이는 여러 스레드가 동시에 이 변수를 업데이트하더라도 값이 일관되게 유지되도록 한다.

NUMA(Non-Uniform Memory Access) 아키텍처의 최적화

현대의 멀티코어 시스템은 NUMA 아키텍처를 사용하는 경우가 많다. NUMA 시스템에서는 프로세서가 로컬 메모리에 접근할 때와 다른 프로세서의 메모리 영역에 접근할 때의 속도가 다르다. 이러한 차이를 효과적으로 관리하기 위해서는 다음과 같은 최적화 기법을 사용할 수 있다.

메모리 배치 최적화

NUMA 시스템에서는 프로세스가 주로 사용하는 메모리를 해당 프로세스가 실행되는 프로세서의 로컬 메모리에 배치하는 것이 중요하다. 이를 위해 프로세스와 메모리의 배치를 최적화하는 기법이 필요하다.

numa_alloc_onnode(size, node_id);

위 함수는 특정 노드에 메모리를 할당함으로써, NUMA 환경에서 메모리 접근 속도를 최적화할 수 있다.

NUMA 친화적 스케줄링

운영체제는 프로세스가 주로 사용하는 메모리의 위치를 고려하여 스레드를 적절한 프로세서에 배치하는 NUMA 친화적 스케줄링을 제공해야 한다. 이러한 스케줄링을 통해 캐시 히트율을 높이고 메모리 접근 시간을 줄일 수 있다.

메모리 할당 정책

실시간 시스템에서는 메모리 할당 및 해제의 시간 복잡도가 예측 가능해야 한다. 이를 위해 다양한 메모리 할당 정책이 사용될 수 있다.

고정 크기 블록 할당

고정 크기 블록 할당은 미리 정의된 크기의 메모리 블록을 할당하고 해제하는 방법으로, 할당 및 해제의 시간 복잡도가 일정한다. 이 기법은 실시간 시스템에서 자주 사용된다.

void* allocate_block(MemoryPool* pool) {
    if (pool->free_index < NUM_BLOCKS) {
        return pool->blocks[pool->free_index++];
    }
    return NULL;
}

메모리 재사용

메모리 재사용 기법은 이미 할당된 메모리 블록을 다시 사용함으로써, 메모리 할당/해제의 빈도를 줄이고 성능을 향상시킨다. 이를 통해 실시간 시스템에서 메모리 관련 오버헤드를 줄일 수 있다.

void reuse_memory_block(MemoryPool* pool, void* block) {
    pool->blocks[--pool->free_index] = block;
}

이러한 기법들은 Preempt RT와 같은 실시간 시스템에서 메모리 관리의 복잡성을 줄이고, 예측 가능한 성능을 제공하기 위해 필수적이다.