C 프로그래밍 언어에서 타입 한정자(type qualifier)는 기존 데이터 타입에 특별한 속성을 부여하는 키워드입니다. C 언어에는 const, volatile, restrict 세 가지 타입 한정자가 있으며, 이 글에서는 그중 volatile과 restrict에 대해 자세히 알아보겠습니다.
volatile 한정자란?
volatile은 변수가 '공유(shared)'되고 있음을 컴파일러에게 알려주는 역할을 합니다. 즉, 어떤 변수를 volatile로 선언하면 그 변수는 프로그램 외부의 다른 개체(하드웨어 레지스터, 인터럽트 서비스 루틴, 멀티스레드 환경의 다른 스레드 등)에 의해 언제든지 참조되거나 변경될 수 있습니다.
따라서 컴파일러는 이러한 변수에 대해 불필요한 최적화를 수행하지 않고, 매번 메모리에서 실제 값을 새로 읽어오도록 코드를 생성합니다.
선언 예시:
volatile int x;
restrict 한정자란?
restrict는 포인터에만 사용할 수 있는 한정자로, C99 표준에서 도입되었습니다. 이 한정자가 붙은 포인터는 해당 포인터가 가리키는 데이터에 접근할 수 있는 사실상 유일한 경로임을 의미합니다. 이러한 보장이 있으면 컴파일러는 더 공격적인 최적화를 수행할 수 있습니다.
restrict가 필요한 이유
먼저 일반 포인터를 사용한 경우를 살펴보겠습니다.
int *ptr;
int a = 0;
ptr = &a;
____
____
____
*ptr += 4; // *ptr += 9로 합칠 수 없음
____
____
____
*ptr += 5;
위 코드에서 컴파일러는 *ptr += 4;와 *ptr += 5; 두 문장을 *ptr += 9; 하나로 합칠 수 없습니다. 그 이유는 두 문장 사이에서 변수 a가 직접적으로 또는 다른 포인터를 통해 변경되었을 가능성을 배제할 수 없기 때문입니다.
restrict 적용 시
int *restrict ptr;
int a = 0;
ptr = &a;
____
____
____
*ptr += 4; // *ptr += 9로 합칠 수 있음
____
____
*ptr += 5;
____
____
반면 restrict를 사용하면 컴파일러는 두 문장을 *ptr += 9; 하나로 안전하게 합칠 수 있습니다. 해당 포인터 외에는 변수에 접근할 수 있는 다른 경로가 없다는 것이 보장되기 때문입니다.
restrict 활용 예제
다음은 restrict 키워드를 사용한 C 프로그램입니다.
#include <stdio.h>
void keyword(int* a, int* b, int* restrict c) {
*a += *c;
// c가 restrict이므로 컴파일러는
// 어셈블리 코드에서 c 주소의 값을
// 다시 로드하지 않습니다.
*b += *c;
}
int main(void) {
int p = 10, q = 20, r = 30;
keyword(&p, &q, &r);
printf("%d %d %d", p, q, r);
return 0;
}
실행 결과
위 프로그램을 실행하면 다음과 같은 결과가 출력됩니다.
40 50 30
함수 keyword에서 c는 restrict 포인터이므로, 컴파일러는 *a += *c; 실행 후 c가 가리키는 값을 다시 메모리에서 읽어오지 않아도 된다고 판단합니다. 이를 통해 불필요한 메모리 접근을 줄여 프로그램의 성능을 향상시킬 수 있습니다.