volatile 타입 한정자
volatile 타입 한정자
C의 타입 시스템에서 각 타입에는 const, volatile, 그리고 객체 타입 포인터에 대한 restrict 한정자 중 하나, 둘, 또는 셋을 조합한 한정된 버전이 존재해요. 이 페이지에서는 그중 volatile 한정자의 효과를 설명해요.
출처: cppreference
본문
volatile 한정 타입의 lvalue 표현식을 통해 이뤄지는 모든 접근(읽기와 쓰기 모두)은 최적화를 위한 **관측 가능한 부작용(observable side effect)**으로 간주돼요. 그리고 추상 기계의 규칙대로 엄격히 평가돼요 (즉 모든 쓰기는 다음 순차 지점(sequence point) 이전 어느 시점에 완료돼요). 이 말은 단일 스레드 실행 안에서, volatile 접근은 그 접근과 순차 지점으로 분리된 다른 보이는 부작용에 대해 최적화로 제거되거나 재배열될 수 없다는 뜻이에요.
비volatile 값을 volatile 타입으로 캐스팅하는 건 아무 효과가 없어요. non-volatile 객체를 volatile 의미론으로 접근하려면, 그 주소를 pointer-to-volatile로 캐스팅한 뒤 그 포인터를 통해 접근해야 해요.
volatile 한정 타입의 객체를 non-volatile lvalue로 읽거나 쓰려고 하면 정의되지 않은 동작이 돼요.
volatile int n = 1; // object of volatile-qualified type
int* p = (int*)&n;
int val = *p; // undefined behavior
volatile 한정 struct/union 타입의 멤버는 그 타입이 속한 한정을 물려받아요 (. 연산자로 접근하든 -> 연산자로 접근하든 마찬가지예요).
struct s { int i; const int ci; } s;
// the type of s.i is int, the type of s.ci is const int
volatile struct s vs;
// the types of vs.i and vs.ci are volatile int and const volatile int
배열 타입이 volatile 한정자로 선언되면(typedef를 통해), 배열 타입 자체는 volatile 한정이 되지 않고 요소 타입만 한정돼요. (C23부터는 배열 타입과 그 요소 타입이 항상 동일하게 volatile 한정된 것으로 간주돼요.)
typedef int A[2][3];
volatile A a = {{4, 5, 6}, {7, 8, 9}}; // array of array of volatile int
int* pi = a[0]; // Error: a[0] has type volatile int*
void *unqual_ptr = a; // OK until C23; error since C23
// Notes: clang applies the rule in C++/C23 even in C89-C17 modes
함수 타입이 volatile 한정자로 선언되면(typedef를 통해) 동작은 정의되지 않아요.
함수 선언에서 volatile 키워드는 함수 매개변수의 배열 타입을 선언하는 대괄호 안에 나타날 수 있어요. 이는 배열 타입이 변환되는 포인터 타입을 한정해요. 다음 두 선언은 같은 함수를 선언해요.
void f(double x[volatile], const double y[volatile]);
void f(double * volatile x, const double * volatile y);
(C99부터)
non-volatile 타입에 대한 포인터는 같은 타입(또는 호환 타입)의 volatile 한정 버전에 대한 포인터로 묵시적으로 변환될 수 있어요. 그 반대 방향의 변환은 캐스트 표현식이 필요해요.
int* p = 0;
volatile int* vp = p; // OK: adds qualifiers (int to volatile int)
p = vp; // Error: discards qualifiers (volatile int to int)
p = (int*)vp; // OK: cast
T에 대한 포인터의 포인터는 volatile T에 대한 포인터의 포인터로는 변환되지 않아요. 두 타입이 호환되려면 한정이 동일해야 하거든요.
char *p = 0;
volatile char **vpp = &p; // Error: char* and volatile char* are not compatible types
char * volatile *pvp = &p; // OK, adds qualifiers (char* to char*volatile)
volatile의 쓰임
-
staticvolatile객체는 메모리 매핑 I/O 포트를 모델링하고,staticconstvolatile객체는 실시간 클록 같은 메모리 매핑 입력 포트를 모델링해요.volatile short *ttyport = (volatile short*)TTYPORT_ADDR; for(int i = 0; i < N; ++i) *ttyport = a[i]; // *ttyport is an lvalue of type volatile short -
sig_atomic_t타입의staticvolatile객체는 시그널 핸들러와의 통신에 쓰여요. -
setjmp매크로를 호출하는 함수에 지역인volatile변수들은longjmp가 돌아온 뒤에도 값을 유지하는 것이 보장되는 유일한 지역 변수예요. -
이에 더해, volatile 변수는 특정 최적화를 비활성화하는 데 쓸 수 있어요. 예를 들어 마이크로 벤치마크에서 죽은 저장 제거(dead store elimination)나 상수 폴딩을 끄는 식이죠.
volatile 변수는 스레드 간 통신에는 적합하지 않아요. 원자성, 동기화, 메모리 순서를 제공하지 않기 때문이에요. 동기화 없이 다른 스레드가 수정하는 volatile 변수를 읽거나, 동기화되지 않은 두 스레드가 동시에 수정하는 것은 데이터 경쟁(data race)으로 인해 정의되지 않은 동작이 돼요.
키워드
volatile
예제
여기서는 volatile 변수를 쓰면 최적화가 꺼져 실제로 연산이 수행되는 모습을, 그렇지 않은 경우와 비교해 확인해 볼게요.
#include <stdio.h>
#include <time.h>
int main(void)
{
clock_t t = clock();
double d = 0.0;
for (int n = 0; n < 10000; ++n)
for (int m = 0; m < 10000; ++m)
d += d * n * m; // reads from and writes to a non-volatile
printf("Modified a non-volatile variable 100m times. "
"Time used: %.2f seconds\n",
(double)(clock() - t)/CLOCKS_PER_SEC);
t = clock();
volatile double vd = 0.0;
for (int n = 0; n < 10000; ++n)
for (int m = 0; m < 10000; ++m) {
double prod = vd * n * m; // reads from a volatile
vd += prod; // reads from and writes to a volatile
}
printf("Modified a volatile variable 100m times. "
"Time used: %.2f seconds\n",
(double)(clock() - t)/CLOCKS_PER_SEC);
}
가능한 출력:
Modified a non-volatile variable 100m times. Time used: 0.00 seconds
Modified a volatile variable 100m times. Time used: 0.79 seconds
더 알아보기
volatile과 함께 가장 흔히 쓰이는 또 다른 한정자인const는 관련 문서에서 다뤄요. 둘을 합친const volatile도 자주 등장해요.- 한정자 제거가 왜 문제인지는 호환 타입(compatible type)의 한정 규칙에서 이어서 볼 수 있어요.
- C++의
cv한정자는const와volatile을 함께 다루니 비교해 보세요.