컴파일러 최적화 Compiler Optimization
컴파일러 최적화는 많은 프로그램에서 널리 쓰이고 있다. VS에서는 소스코드를 Release로 빌드하는 것만으로 손 쉽게 최적화를 적용할 수 있으며 프로그램의 성능 향상에 큰 도움을 준다. 그러나 멀티스레드 프로그래밍에서 이러한 최적화는 예기치 못한 버그를 발생시킨다.
원본 소스코드
#include <iostream>
#include <thread>
using namespace std;
bool g_IsTurnOn = false;
void TurnOn()
{
g_IsTurnOn = true;
}
void Run()
{
while(!g_IsTurnOn); // TurnOn()이 호출되기 전까지 대기
cout << "Turn On!";
}
void main()
{
thread t1{Run};
thread t2{TrunOn};
t1.join();
t2.join();
getchar();
}
위 코드는 TurnOn()
메서드가 호출될 때 까지 기다렸다가 Turn On! 메시지를 출력하고 종료되는 단순한 프로그램이다.
이 예제를 실행하면 프로그램이 무한 루프에 빠지는 버그가 발생한다. 왜 그럴까?
원인 분석
원인을 분석하기 위해 while(!g_IsTrunOn);
부분에 중단점을 걸고 실행하자.
중단점에 걸리면 디스어셈블리를 확인할 수 있다.
디스어셈블리는 디버그(D) -> 창(W) -> 디스어셈블리(D) 에 있다.
이 부분에서는 제대로 메모리 ds:[01064490h]
에 1을 대입하고 있다.
그러나 Line 15 윗 줄에 보면 010612C1 mov al,byte ptr ds:[01064490h]
를 하고있는데 이 코드는 메모리 ds:[01064490h]
에 있는 값을 지역변수 al로 mov한다는 의미이다. 이후 Line 15를 살펴보면 자신의 지역변수 al과 al끼리 비교한다.
컴파일러는 왜 프로그래머가 시키지도 않은 지역변수 al로 값을 복사해 오는 것일까? 그 이유는 바로 컴파일러 최적화가 싱글 코어 프로그래밍을 위해 만들어졌기 때문이다. 그렇다면 이것은 컴파일러의 버그일까? 그렇지는 않다. 이러한 컴파일러 최적화는 싱글 코어 프로그래밍에서는 정상적으로 동작하며, 이로 인해 많은 프로그램들이 상당한 퍼포먼스 향상을 누리고 있기 때문이다.
문제 해결
위와 같은 문제는 컴파일러 최적화를 명시적으로 거부하는 volatile 키워드를 사용하는 것으로 해결할 수 있다.
volatile bool g_IsTurnOn = false;
이 키워드를 사용한 뒤 디스어셈블리를 보면 다음과 같다.
ds:[10D4490h]
번지에 1값을 넣고 Line 15에서도 제대로 해당 번지의 값을 이용하고 있다.
이처럼 컴파일러는 프로그래머가 의도치 않았던 최적화를 수행하며, 이는 싱글 스레드 프로그램이라면 아무런 문제가 없을 뿐 아니라 성능 향상에 큰 도움을 준다. 그러나 멀티스레드 프로그밍에서는 별도로 공부하지 않고서는 잡을 수 없는 버그의 원인이 되기도 한다.