티스토리 뷰

/* 본 포스팅은 최호성 저자의 '독하게 시작하는 C프로그래밍' 교재를 참조로 작성되었음을 먼저 알려드립니다. */


 형한정어 volatile을 적용해 변수를 선언하면 변수와 관련된 모든 연산에 대해 컴파일러가 '최적화' 규칙을 적용하지 않는다. 설령 그것이 컴파일러가 판단하기에 전혀 불필요한 연산이라고 해도. 컴파일러의 최적화에 도움을 주는 const와는 정반대의 역할을 수행하는 것으로 봐도 된다.


 '최적화'를 하지 않는 수준은 CPU 기계 수준까지 적용된다. CPU는 빠른 연산을 위해 캐시(cache)메모리를 사용하는데, volatile로 선언된 변수는 캐시로 처리되지 못한다. 효율은 떨어지겠지만 주기억장치(메모리)에 정말로 정보가 읽고 쓰이는 행위는 보장이되는 셈이다.


 비효율적이지만 꼭 해야할 연산이 있을 수 있다. 그러나 컴파일러가 불필요한 요소라고 판단해버리면 소스코드의 의미가 그대로 기계어로 번역되지 못하는 경우가 발생한다. 이런 경우는 주로 임베디드 프로그래밍 환경에서 일어난다. 만약 AVR을 제어하는 프로그램이나 운영체제를 개발해야 한다면, 이 형한정어 volatile에 대해 잘 알고 있어야할 것이다.


 아래 예제는 반복문을 100회 반복하면서 nData에 계속 10을 대입한 후 출력하는 프로그램이다. 몇 백 아니 몇 만 번을 수행한다 해도 nData에 담긴 값을 출력하면 늘 10일 수밖에 없는 매우 비효율적인 프로그램이다.

#include <stdio.h>
int main(void)
{
	int nData = 10, i = 0;

	for (i = 0; i < 100; i++)
		nData = 10;

	printf("%d\n", nData);

	return 0;
}

 7번 행을 아무리 많이 반복해서 실행해도 10번 행에서 출력하는 값이 10이라는 사실은 변하지 않는다. 코드 자체의 문제도 있지만, 이러한 문제는 컴파일러도 인식한다.


 이 예제를 작성했다면 5번 행에 위치 중단점(F9키)을 설정하고 디버그 모드(F5)로 프로그램을 실행한 후, 중단점이 적중했을 때 아래와 같이 '디스어셈블리(disassembly)' 화면을 확인하면 모든 코드가 기계어로 번역된 내용을 확인할 수 있다. // 비주얼 스튜디오에서 디버그 모드로 실행한 후, 디버그 메뉴의 창을 눌러보면 맨 아래쪽에 디스어셈블리라고 적혀있는 부분을 누르면 볼 수 있다.

       int nData = 10, i = 0;
0039179E  mov         dword ptr [nData],0Ah 
003917A5  mov         dword ptr [i],0 
       for (i = 0; i < 100; i++)
003917AC  mov         dword ptr [i],0 
003917B3  jmp         main+3Eh (03917BEh) 
003917B5  mov         eax,dword ptr [i] 
003917B8  add         eax,1 
003917BB  mov         dword ptr [i],eax 
003917BE  cmp         dword ptr [i],64h 
003917C2  jge         main+4Dh (03917CDh) 
              nData = 10;
003917C4  mov         dword ptr [nData],0Ah 
003917CB  jmp         main+35h (03917B5h) 

 디버그 모드로 프로그램을 빌드했을 때는 논리적으로 불필요한 부분이든 아니든 무조건 기계어로 번역한다. 그러나 '최적화'규칙을 적용하여 번역(릴리즈 모드)하면 컴파일러가 스스로 쓸데없는 코드라고 판단한 코드들은 아예 번역조차 하지 않고 그냥 넘어가 버린다. 프로그램을 릴리즈 모드로 번역하려면 설정을 변경해야 하는데 쉽게는 도구 모음에 있는 'Debug'를 'Release'로 변경하면 된다.


 그러고 나서 프로그램을 다시 빌드한 후 디버그 모드로 실행(F5)한다. 이어서 위치 중단점이 적중하면 다시 디스어셈블리 창을 확인한다. 그러면 아래와 같이 대부분의 코드가 아예 번역조차 되지 않았음을 확인할 수 있다.

       int nData = 10, i = 0;
       for (i = 0; i < 100; i++)
              nData = 10;
       printf("%d\n", nData);
01361040  push        0Ah 
01361042  push        offset string "%d\n" (013620F8h) 
01361047  call        printf (01361010h) 
0136104C  add         esp,8 
       return 0;
0136104F  xor         eax,eax 

 반복문은 아예 수행하지도 않았고 변수 nData, i 모두 존재하지도 않는다. 프로그램의 결과로 화면에 10이 출력되어야 하므로 기계어로 보면 printf( )함수의 인수로 단지 '10(0Ah)'을 넘겨서 결과만 일치하도록 맞춰버렸다. 결국 반복문은 번역 과정에서 아예 사라진 것이다.


 디버그 모드로 빌드하면 전혀 문제가 없던 프로그램이 릴리즈 모드로 빌드하는 순간 상상하지도 못한 버그들이 발견되거나 기술적으로 설명할 수 없는 결과가 야기되는 일이 종종 벌어진다. 그에 대한 상당수 원인이 바로 이 '최적화' 때문이다. 위의 예제를 수정하여 nData의 선언에 형한정어 volatile을 적용하고 다시 릴리즈 모드로 빌드한 후, 결과를 확인해보자.

#include <stdio.h>
int main(void)
{
	volatile int nData = 10, i = 0;

	for (i = 0; i < 100; i++)
		nData = 10;

	printf("%d\n", nData);

	return 0;
}

 수정한 후 다시 빌드하여 디스어셈블리창(Ctrl + Alt + D)을 확인해 보면 아래와 같이 nData가 확실히 존재하고, 반복문을 100회 반복하여 nData에 10을 대입하도록 기계어로 번역되었음을 확인할 수 있다. volatile 예약어에 의해 nData와 관련된 모든 연산에 대해 최적화 규칙을 적용하지 않았기 때문이다.

int main(void)
{
01311040  push        ebp 
01311041  mov         ebp,esp 
01311043  sub         esp,8 
       volatile int nData = 10, i = 0;
01311046  mov         dword ptr [ebp-8],0Ah 
0131104D  mov         dword ptr [ebp-4],0 
       for (i = 0; i < 100; i++)
01311054  mov         dword ptr [i],0 
0131105B  cmp         dword ptr [i],64h 
0131105F  jge         main+31h (01311071h) 
              nData = 10;
01311061  mov         dword ptr [nData],0Ah 
01311068  inc         dword ptr [i] 
0131106B  cmp         dword ptr [i],64h 
0131106F  jl          main+21h (01311061h) 


댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday