기본 패턴
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/O | volatile |
| 시그널 핸들러가 수정하는 플래그 | 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