C시작과 문법

volatile과 restrict

컴파일러가 레지스터에 캐싱하지 못하도록 강제하는 `volatile`, 포인터가 겹치지 않음을 약속해 벡터화를 허용하는 `restrict`의 동작 원리와 사용 시점을 정리합니다.

마지막 수정 2026년 3월 26일

기본 패턴

c
// volatile — 매번 실제 메모리에서 읽도록 강제
volatile uint32_t *STATUS_REG = (volatile uint32_t *)0x40000000;
while ((*STATUS_REG & 0x01) == 0) { /* 장치 준비 대기 */ }

// restrict — 두 포인터가 같은 메모리를 가리키지 않음을 컴파일러에 약속
void copy(int * restrict dst, const int * restrict src, int n) {
    for (int i = 0; i < n; i++) dst[i] = src[i];
}

설명

volatile — 컴파일러 캐싱 억제

컴파일러는 반복되는 메모리 읽기를 레지스터에 한 번 캐싱하는 방식으로 최적화합니다. 하드웨어가 직접 값을 바꾸는 레지스터나 시그널 핸들러가 수정하는 변수라면 이 최적화가 잘못된 동작을 만듭니다. volatile을 붙이면 매 접근마다 실제 메모리(또는 MMIO 주소)를 읽도록 강제합니다.

c
// volatile 없는 경우 — 컴파일러가 아래처럼 "최적화"할 수 있음
// int cached = READY_FLAG;
// while (cached == 0) { }  // 영원히 탈출 못 함

volatile int READY_FLAG = 0;   // 하드웨어나 다른 스레드가 1로 바꿈

// ✅ 매 반복마다 실제 메모리를 읽음
while (READY_FLAG == 0) {
    /* 대기 */
}

volatile의 세 가지 주요 사용처:

c
// 1. 하드웨어 레지스터 (메모리 매핑 I/O)
volatile uint8_t *PORT = (volatile uint8_t *)0xFF20;
*PORT = 0xFF;   // 쓰기가 최적화로 제거되지 않음

// 2. 시그널 핸들러에서 수정되는 플래그
#include <signal.h>
volatile sig_atomic_t g_stop = 0;
void handler(int sig) { g_stop = 1; }
// main 루프: while (!g_stop) { ... }

// 3. setjmp/longjmp 사용 시 지역 변수 보호
volatile int result = 0;   // longjmp 후에도 값 보존

restrict — 포인터 앨리어싱 없음을 약속

두 포인터가 같은 메모리를 가리킬 수 있는지(앨리어싱) 모르면 컴파일러는 보수적으로 동작합니다. restrict는 "이 포인터가 가리키는 메모리에는 이 포인터를 통해서만 접근한다"는 약속입니다. 컴파일러가 SIMD 벡터화 같은 공격적인 최적화를 적용할 수 있게 됩니다.

c
// restrict 없음 — dst와 src가 겹칠 가능성 때문에 최적화 보수적
void copy_safe(int *dst, const int *src, int n) {
    for (int i = 0; i < n; i++) dst[i] = src[i];
}

// restrict 있음 — 절대 겹치지 않음을 보장 → 벡터화 가능
void copy_fast(int * restrict dst, const int * restrict src, int n) {
    for (int i = 0; i < n; i++) dst[i] = src[i];
}

// 표준 라이브러리 memcpy도 restrict를 사용 (겹침 UB의 이유)
// void *memcpy(void * restrict dst, const void * restrict src, size_t n);

빠른 정리

상황적합한 선택
하드웨어 레지스터, 메모리 매핑 I/Ovolatile
시그널 핸들러가 수정하는 플래그volatile sig_atomic_t
멀티스레드 동기화_Atomic (C11) 또는 mutex — volatile 아님
두 포인터가 겹치지 않음을 보장할 수 있을 때restrict
표준 memcpy/memset 대신 수동 루프 최적화restrict

주의할 점

volatile은 원자성을 보장하지 않습니다. 멀티스레드 동기화에는 _Atomic 또는 뮤텍스를 사용해야 합니다.

c
volatile int counter = 0;

// ❌ 두 스레드가 동시에 실행하면 증가값이 소실될 수 있음
// read → modify → write 사이에 컨텍스트 스위치 가능
counter++;

// ✅ C11 원자 타입 사용
#include <stdatomic.h>
atomic_int counter2 = 0;
atomic_fetch_add(&counter2, 1);   // 원자적 증가

restrict를 잘못 사용해 실제로 겹치는 포인터에 적용하면 정의되지 않은 동작입니다.

c
int arr[10] = {1, 2, 3, 4, 5};

// ❌ src와 dst가 겹침 — restrict 약속 위반 → 정의되지 않은 동작
copy_fast(arr + 2, arr, 5);   // dst=arr+2, src=arr — 겹침

// ✅ 겹치는 영역에는 memmove 또는 restrict 없는 함수 사용
memmove(arr + 2, arr, 5 * sizeof(int));

참고 링크

1 sources