본문 바로가기

Programming/C&CPP

C++ volatile 키워드 사용

반응형

C++ volatile 키워드는 다른 키워드에 비해서 잘 사용되지 않는 편입니다.

일반적으로 volatile 키워드는 해당 변수의 컴파일러 최적화를 제한하는 용도로 사용합니다.

컴파일러는 훌륭하게 최적화를 실행하지만 volatile을 사용해서 최적화를 제한해야 하는 상황이 있습니다.


1. 첫 번째 예제

먼저 다음과 같이 멀티스레드 환경의 경우입니다.

#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;
}

먼저 전역 변수 i를 하나 선언합니다.

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으로 조건이 항상 거짓이 되기 때문입니다.

이럴 때 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;
}

i를 선언하는 세 부분에 volatile 키워드를 추가했습니다.

다시 실행하면 원하는 방향으로 동작하는 것을 확인할 수 있습니다.


2. 두 번째 예제

멀티스레드가 아닌 경우에도 다음과 같이 상황이 발생할 수 있습니다.

임베디드 환경에서 하드웨어의 특정 주소의 값을 입력해서 하드웨어를 제어하는 경우입니다.

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;
}

이제 다시 어셈블리 코드를 확인하면 결과가 다르게 표시되는 것을 확인할 수 있습니다.

컴파일러 최적화없이 모든 코드에 대한 어셈블리 코드가 생성되는 것을 확인할 수 있습니다.


volatile 키워드를 사용해야 하는 상황은 많지 않습니다.

다만 volatile 키워드를 사용하는 상황에 대한 이해는 필요합니다.

원하는 코드를 작성했음에도 컴파일러 최적화로 다른 동작이 발생할 때 좀 더 빠르게 해결할 수 있습니다.

반응형