C++/volatile 키워드에 대해

C/C++ 에 있는 키워드 중에서 가장 사용 빈도가 낮은 키워드라고 알려져 있다.

의미
volatile 키워드는 해당 변수가 언제든지 예기치 않게 변경될 수 있음을 컴파일러에 알리는 역할을 한다.
따라서 컴파일러는 변수에 최적화를 수행하지 않는다. 즉, 컴파일러가 값을 예측하거나 캐싱하는 것을 방지하기 때문에 레지스터를 사용하지 않고 매번 메모리에서 읽는다
volatile 키워드를 이해하려면 C/C++ 컴파일러가 변수에 대해 어떻게 최적화를 하는지 알아야 한다.

C/C++의 최적화
영리한 컴파일러라면 코드에서 같은 주소의 메모리를 여러 번 읽고, 마지막에 한번 쓰는 걸 발견하면 속도를 향상 시키기 위해 최종적으로 불필요한 읽기를 제거하고 마지막에 쓰기만 수행할 것이다. 일반적인 코드라면 이런 최적화를 통해 수행 속도 면에서 이득을 보게 된다.

하지만 임베디드 장비처럼 메모리 주소에 연결된 하드웨어 값을 읽어온다면(Memory-mapped I/O) 이야기가 달라진다. 언제든지 값이 변경될 수 있기 때문에 주소가 같다는 이유 만으로 메모리 읽기를 예측하거나 캐싱하여 넘기면 안된다. 이런 경우 유용하게 사용될 수 있는 키워드가 volatile이다.

volatile이 사용되는 3가지 상황
  1. 하드웨어 레지스터와 상호 작용하는 경우: 특정 하드웨어 레지스터에 값을 읽고 쓰는 데 사용되는 변수는 예기치 않게 변경될 수 있다.
  2. 멀티스레드 환경에서 공유되는 변수: 여러 스레드가 동시에 접근할 수 있는 변수는 언제든지 다른 스레드에 의해 변경될 수 있다.
  3. 인터럽트 서비스 루틴: 인터럽트가 발생하면 실행 중인 코드가 중단되고 인터럽트 서비스 루틴이 실행된다. 이 경우 인터럽트 서비스 루틴에서 사용되는 변수는 예기치 않게 변경될 수 있으므로 volatile로 선언해야 한다.

세 가지 공통점은 현재 프로그램이 실행 중인 코드와 상관없이 외부 요인에 의해 변수 값을 변경할 수 있다는 점이다.
이처럼 레지스터를 재사용하지 않고 매번 메모리를 참조하는 것을 가시성(visibility)이 보장된다고 말한다.

원자성
(!) 주의할 점은 volatile 키워드는 스레드 간의 메모리 동기화 문제를 해결해 주지는 않는다. 즉, 데이터 경쟁 조건이나 원자성을 보장하지 않는다.

멀티스레드 환경도 MMIO와 마찬가지로 프로그램 수행 도중 다른 스레드가 전역 변수 값을 임의로 변경할 수 있다. volatile 키워드가 컴파일러에게 변수가 언제든지 변경될 수 있음을 알려주는 역할을 하기에 이를 해결해 줄 수 있을 것만 같다.
그러나 여러 스레드가 공유하는 전역 변수의 경우, volatile 키워드만으로는 충분하지 않다.

이러한 동기화 문제를 해결하기 위해서는 C++11 이후의 표준에서 도입된 std::atomic 타입이나 뮤텍스, std::lock_guard와 같은 동기화 메커니즘으로 명시적으로 락을 잡아 사용해야 한다.

재배치Reordering
volatile은 표준에서 키워드와 메모리 모델에 대해 명확한 정의를 내리지 않았기 때문에 컴파일러마다 구현에 차이가 있다. MSVC는 volatile에 가시성 뿐만 아니라 재배치Reordering 문제에 대한 해결책도 추가하였다.

재배치는 컴파일러가 메모리 접근 속도 향상, 파이프라인 활용 등 최적화 목적으로 프로그램 명령의 위치를 바꾸는 것을 말하는데, 코드 상에 a = 1; b = 1; c = 1;라고 작성했다고 해서 반드시 abc 순서로 메모리에 쓰지 않을 수도 있다는 의미다. 캐시 상황 등에 따라 속도 향상을 볼 수 있다면 컴파일러는 a,c를 먼저 쓰고 b를 쓰는 순서가 바꿀 수 있는 것이다.

volatile을 사용하면 컴파일러가 수행하는 재배치에 제약을 준다.

volatile은 단일 CPU 환경에서 재배치 문제는 해결해 주지만 멀티 CPU의 재배치에 대해서는 완전한 대안을 제공해주지는 못한다.

유용성과 한계를 충분히 인지하고 volatile을 사용하자.


이 글에는 0 개의 댓글이 있습니다.