개념
const 키워드와 함께 변수의 성질을 바꾸는 역할을 하는 타입 한정자지만 그 사용 빈도가 낮아 책이나 자료들에서도 잘 다루어지지 않는 타입이다.
volatile 키워드가 지정된 변수는 최적화를 수행하지 않는다.
변수의 최적화
최적화를 시켜주는 컴파일러의 기능인데 프로그래머는 사람이기 때문에 실수를 하기 마련이다.
물론 컴파일러가 모든걸 보완할 수는 없다.
예를 들면)
int a;
a = 0;
a = 1;
a = 3;
a에는 최종적으로 3의 값이 들어가게 되며 이전의 작업인 0과 1은 의미가 없게 된다, 따라서 재정의를 하는 경우에는 컴파일러가 알아서 위의 두 작업을 삭제한다. 이를 통해 수행 시간의 이득을 가져올 수 있다.
하지만, 만약 메모리를 참조하여 하드웨어에 명령을 내리는 코드라고 가정하고 a를 메모리 쓰기 변수라고 하면, 명령어 자체가 하나의 지시 기능을 가지게 된다. 따라서, a = 0; a = 1; 의 두 작업이 최적화를 통해 없어진다면 0, 1에 해당하는 작업을 실행하지 못하게 될 것이다.
volatile int a = 0;
a = 0;
a = 1;
a = 3;
이렇게 선언하게 된다면, 변수 대입 작업을 전부 실행하게 된다. - 유사하게 하드웨어 메모리에서 변하는 값을 가져와 실행하는 변수들에도 volatile 키워드는 필수다. 즉, 외부 요인에 의해 변수 값이 변경될 가능성이 있는 변수에는 volatile을 써줘야 한다.
- Memory-mapped I/O
- 인터럽트 서비스 루틴
- 멀티 쓰레드 환경
등등..
예제
멀티스레드 환경
Func1()은 i가 1이 되면 화면에 출력을 하고 Func2()는 1초마다 i를 0 <-> 1로 변경한다.
각각 Func1()과 Func2()를 실행하는 스레드를 생성하고 대기한다.
의도한 내용은 Func2()에서 i를 1로 변경하는 1초마다 Func1()이 카운트를 화면에 표시해주는 것이다.
최적화 옵션을 끄면 정상적으로 동작하지만 최적화를 켜면 동작하지 않는 컴파일러들이 존재한다.
Visual Studio 2017 기준으로 컴파일러 최적화 옵션(/O2)를 지정하면 출력이 전혀 되지 않는다.
해당 문제의 원인은 Func1() 내에서 i가 0으로 초기화되고 변경되지 않기 때문이다.
컴파일러 최적화로 1 == i 부분의 i가 0으로 치환되기 때문에 1 == 0으로 조건이 항상 거짓이 되기 때문이다.
#include <thread>
#include <iostream>
#include <chrono>
int i;
void Func1()
{
extern int i;
int count = 0;
i = 0;
while (true)
{
if (1 == i)
std::cout << "Count : " << ++count << std::endl;
}
}
void Func2()
{
extern int i;
i = 0;
while (true)
{
std::chrono::seconds sleepSec(1);
std::this_thread::sleep_for(sleepSec);
i = !i;
}
}
int main()
{
std::thread th1(Func1);
std::thread th2(Func2);
th1.join();
th2.join();
return 0;
}
이럴 때 volatile 키워드를 사용하면 컴파일러의 최적화를 막을 수 있다.
#include <thread>
#include <iostream>
#include <chrono>
volatile int i;
void Func1()
{
extern volatile int i;
int count = 0;
i = 0;
while (true)
{
if (1 == i)
std::cout << "Count : " << ++count << std::endl;
}
}
void Func2()
{
extern volatile int i;
i = 0;
while (true)
{
std::chrono::seconds sleepSec(1);
std::this_thread::sleep_for(sleepSec);
i = !i;
}
}
int main()
{
std::thread th1(Func1);
std::thread th2(Func2);
th1.join();
th2.join();
return 0;
}
두 번째 예제
멀티스레드가 아닌 경우에도 다음과 같이 상황이 발생할 수 있다. 임베디드 환경에서 하드웨어의 특정 주소의 값을 입력해서 하드웨어를 제어하는 경우다.
int main()
{
unsigned int* led_control = new unsigned int;
*led_control = 0x0001;
*led_control = 0x0010;
*led_control = 0x0100;
return 0;
}
특정 메모리 영역에 값을 연속으로 변경하는 코드입니다.
Visual Studio 2017에서 /O2로 최적화를 진행하고 어셈블리 코드를 보면 다음과 같이 표시됩니다.
최적화된 코드를 보면 0x0001과 0x0010은 아예 무시가 되고 있다. 변경된 값을 중간에 사용하는 부분이 없기 때문이다.
일반적인 상황에서는 문제가 되지 않지만 하드웨어 제어의 상황에서는 문제가 생길 수가 있다. 이 경우에도 동일하게 volatile을 사용하면 된다.
int main()
{
volatile unsigned int* led_control = new unsigned int;
*led_control = 0x0001;
*led_control = 0x0010;
*led_control = 0x0100;
return 0;
}
이제 다시 어셈블리 코드를 확인하면 결과가 다르게 표시되는 것을 확인할 수 있다.
컴파일러 최적화없이 모든 코드에 대한 어셈블리 코드가 생성되는 것을 확인할 수 있다.
'프로그래밍 언어 > C++' 카테고리의 다른 글
[C++] false sharing이란? (거짓 공유) (0) | 2025.03.01 |
---|---|
[C++] cin.ignore와 버퍼에 대한 이해 (0) | 2024.12.02 |
[C++] std::map을 value 기준으로 정렬하기 (0) | 2024.12.01 |
[C++] set, map 정렬 기준 바꾸는 방법 (0) | 2024.12.01 |
[C++] 문자열 뒤집는 방법 (0) | 2024.11.13 |