본문 바로가기

Hop the wag during working

volatile '휘발성의' 변수

volatile 지금까지 잘못알고 있었다. 다시 한번 개념을 잡기 위해 여러 사이트를 헤매다 정리하면


이 volatile 선언 변수가 없는 경우 가장 관계가 있는 것이 최적화로 인한 의도한 코드가 삭제 되는 현상이 발생하여 개발자를 애 먹이는 경우가 있다. ARM의 프로그램 코드를 작성하여 다운하고 리셋했는데 의도한 대로 진행하지 않았다. 결국 이 volatile을 넣고서야 제대로 동작 하였다.

 

다음은 사이트를 검색하여 퍼온 내용이다.

================================================================================

출처 : http://www.debuglab.com/knowledge/volatile.html


 1. 요약

Volatile은 ‘휘발성의’ 라는 뜻을 가지고 있습니다.

Volatile 키워드를 사용해서 정의한 변수는 그것을 사용하는 문장(statement)외에 다른 것에 의해서 변경될 수 있다는
의미를 갖습니다.

다른 것이란 운영체제, 하드웨어 혹은 다른 스레드가 될 수 있습니다. 그러므로 Volatile 키워드를 사용해서 정의한 변수는
눈에 보이는 문장만을 상대로 함부로 최적화 시키지 말아야 함을 의미합니다.

이 글에서는 간단한 사용법과 Volatile 키워드의 유무에 따라서 컴파일러가 생산해내는 코드가 어떻게 달라지는지  
알아보겠습니다.


2. 본문

(1) 사용법

사용법은 간단합니다.

volatile int k; 
이제 k는 컴파일러가 함부로 최적화 시키지 않습니다.

(2) Loop를 돌며 일하는 Worker Thread

프로세스가 생성되면서 함께 생성되는 메인 스레드가 아니라면, 대부분의 경우는 메인스레드가 신호를 보내줄 때까지  
루프를 돌면서 반복작업을 합니다.

다음과 같은 스레드 입구(Entry)함수가 있다고 합시다.

void ThreadEntry(void* pParm)
{
bool* pbExit = static_cast<bool*>(pParm);
int i = 0;
while( !*pbExit) {
i++;
}
printf("i = %d\n" ,i);
}

메인 스레드가 넘겨준 bool 변수가 true값을 가질 때까지 계속해서 i의 값을 1씩 더하는 루틴입니다.
*pbExit가 true가 되는 순간에 while문을 빠져 나오고 이 워커 스레드( 이렇게 부르기로 합시다)는 종료하게 됩니다.

물론 Debug 모드로 컴파일 하신다면 위의 시나리오대로 잘 작동할 겁니다.
하지만 Release 모드로 컴파일 하신다면 아마도 워커 스레드는 정상으로 종료할 수 없을 겁니다.

그 이유는 Release 모드에서는 컴파일러가 최적화를 하기 때문입니다. 위의 코드를 다시 보신다면,  
함수 내에서 *pbExit 의 값을 변경시키는 부분( 읽는 부분만 있죠)은 없다는 것을 알 수 있습니다.

그 결과 컴파일러는 굳이 *pbExit의 값을 매번 비교할 필요가 없다고 생각하고는 *pbExit가 true인지 비교하는  
코드를 제거해버린 것입니다.

해결책은 다음과 같습니다.

volatile bool* pbExit = static_cast<volatile bool*>(pParm); 

이제 컴파일러는 섣불리 최적화하지 않을 것이고, Release 모드에서 정상 종료하는 좋은 코드가 되었습니다.
volatile의 유무에 따라 컴파일러가 생산하는 코드가 3. 예제 코드에 있으니 참조하시기 바랍니다.

Release 모드에서 어셈블된 코드를 보시려면 컴파일러 옵션에서 /FAcs를 추가하시면 됩니다. 그러면 *.cod라는  
이름의 파일이 생성됩니다.


3. 예제 코드

(1) volatile없는 경우

 ; 12 : bool* pbExit = static_cast<bool*>(pParm);
; 13 : int i = 0;
; 14 :
; 15 : while( !*pbExit)
00000 8b 4c 24 04 mov ecx, DWORD PTR _pParm$[esp-4]
00004 33 c0 xor eax, eax
00006 80 39 00 cmp BYTE PTR [ecx], 0
00009 75 03 jne SHORT $L42424
$L42423:
; 16 : {
; 17 : i++;
0000b 40 inc eax
0000c eb fd jmp SHORT $L42423 // 이 부분 보이시죠? 비교도 안하고 무조건 점프하는 모습!!
$L42424: 
; 18 :
}

(2) volatile있는 경우

 ; 12 : volatile bool* pbExit = static_cast<volatile bool*>(pParm); 
; 13 : int i = 0;
; 14 :
; 15 : while( !*pbExit)
00000 8b 4c 24 04 mov ecx, DWORD PTR _pParm$[esp-4]
00004 33 c0 xor eax, eax
00006 80 39 00 cmp BYTE PTR [ecx], 0
00009 75 07 jne SHORT $L42424
$L42423:
; 16 : {
; 17 : i++;
0000b 40 inc eax
////////////////////////////////////////////////
// 이 부분 보이시죠?? 비교해서 분기하는 모습!!
0000c 8a 11 mov dl, BYTE PTR [ecx]
0000e 84 d2 test dl, dl
00010 74 f9 je SHORT $L42423
$L42424:
; 18 :
}



- 2001.08.06 Smile Seo -


================================================================================

출처 : http://www.winapi.co.kr/clec/cpp2/15-1-4.htm

volatile

volatile 키워드는 const와 함께 변수의 성질을 바꾸는 역할을 하는데 이 둘을 묶어 cv 지정자(Qualifier:제한자라고 번역하기도 한다)라고 한다. const에 비해 상대적으로 사용 빈도가 지극히 낮으며 이 키워드가 꼭 필요한 경우는 무척 드물다. 어떤 경우에 volatile이 필요한지 다음 코드를 보자.

 

int i;

double j;

 

for (i=0;i<100;i++) {

     j=sqrt(2.8)+log(3.5)+56;

     // do something

}

 

이 코드는 루프를 100번 실행하면서 어떤 작업을 하는데 루프 내부에서 j에 복잡한 연산 결과를 대입하고 있다. j값을 계산하는 식이 조금 복잡하지만 제어 변수 i값을 참조하지 않기 때문에 i 루프가 실행되는동안 j의 값은 상수나 마찬가지이며 절대로 변경되지 않는다. 루프 실행중에는 상수이므로 이 값을 매 루프마다 다시 계산하는 것은 시간 낭비이다. 그래서 제대로 된 컴파일러는 이 루프를 다음과 같이 수정하여 컴파일한다.

 

j=sqrt(2.8)+log(3.5)+56;

for (i=0;i<100;i++) {

     // do something

}

 

j의 값을 계산하는 식을 루프 이전으로 옮겨서 미리 계산해 놓고 루프 내부에서는 j값을 사용하기만 했다. 어차피 루프 내부에서 j값이 바뀌는 것이 아니므로 이렇게 코드를 수정해도 원래 코드와 완전히 동일한 동작을 할 것이다. 똑똑한 컴파일러는 프로그래머가 코드를 대충 짜 놓아도 속도를 높이기 위해 자동으로 최적화를 하는 기능을 가지고 있으며 이런 암묵적인 최적화 기능에 의해 프로그램의 성능이 향상된다.

그렇다면 위 두 코드가 정말로 완전히 동일할까 의심을 가져 보자. j는 분명히 루프 내부에서 상수이므로 미리 계산해 놓아도 아무 문제가 없음이 확실하다. 그러나 아주 특수한 경우 최적화된 코드가 원래 코드와 다른 동작을 할 경우가 있다. 어떤 경우인가 하면 프로그램이 아닌 외부에서 j의 값을 변경할 때이다.

도스 환경에서는 인터럽트라는 것이 있고 유닉스 환경에서는 데몬, 윈도우즈 환경에서는 서비스 등의 백그라운드 프로세스가 항상 실행된다. 이런 백그라운드 프로세스가 메모리의 어떤 상황이나 전역변수를 변경할 수 있으며 같은 프로세스 내에서도 스레드가 여러 개라면 다른 스레드가 j의 값을 언제든지 변경할 가능성이 있다. 또한 하드웨어에 의해 전역 환경이 바뀔 수도 있다.

예를 들어 위 코드를 실행하는 프로세스가 두 개의 스레드를 가지고 있고 다른 스레드에서 어떤 조건에 의해 전역변수 j값(또는 j에 영향을 미치는 다른 값)을 갑자기 바꿀 수도 있다고 하자. 이런 경우 루프 내부에서 매번 j값을 다시 계산하는 것과 루프에 들어가기 전에 미리 계산해 놓는 것이 다른 결과를 가져올 수 있다. i루프가 50회째 실행중에 다른 스레드가 j를 바꾸어 버릴 수도 있는 것이다.

이런 경우에 쓰는 것이 바로 volatile이다. 이 키워드를 변수 선언문 앞에 붙이면 컴파일러는 이 변수에 대해서는 어떠한 최적화 처리도 하지 않는다. 컴파일러가 보기에 코드가 비효율적이건 어쨌건 개발자가 작성한 코드 그대로 컴파일한다. 즉 volatile 키워드는 "잘난척 하지 말고 시키는 대로 해"라는 뜻이다. 어떤 변수를 다른 프로세스나 스레드가 바꿀 수도 있다는 것을 컴파일러는 알 수 없기 때문에 전역 환경을 참조하는 변수에 대해서는 개발자가 volatile 선언을 해야 한다. 위 코드에서 j 선언문 앞에 volatile만 붙이면 문제가 해결된다.

 

volatile double j;

 

이 키워드가 반드시 필요한 상황에 대한 예제를 만들어 보이는 것은 굉장히 어렵다. 왜냐하면 외부에서 값을 바꿀 가능성이 있는 변수에 대해서만 이 키워드가 필요한데 그런 예제는 보통 크기가 아니기 때문이다. 잘 사용되지 않는 키워드이므로 여기서는 개념만 익혀 두도록 하자.

 

================================================================================
http://www.phim.unibe.ch/comp_doc/c_manual/C/SYNTAX/volatile.html

  • The volatile keyword acts as a data type qualifier. The qualifier alters the default why in which the compiler handles the variable and does not attempt to optimize the storage referenced by it. Martin Leslie
  • volatile means the storage is likely to change at anytime and be changed but something outside the control of the user program. This means that if you reference the variable, the program should always check the physical address (ie a mapped input fifo), and not use it in a cashed way.Stephen Hunt
  • Here is an example for the usage of the volatile keyword:
     /* Base address of the data input latch */ 

     volatile unsigned char *baseAddr;  /* read parts of output latch */

      lsb = *handle->baseAddr;

      middle = *handle->baseAddr;

      msb = *handle->baseAddr;

    Between reads the bytes are changed in the latch.

    Without the volatile, the compiler optimises this to a single assignment:

     lsb = middle = msb = *handle->baseAddr; 
    Tim Potter
  • A volatile variable is for dynamic use. E.G. for data that is to be passed to an I/O port Here is an example.
     #define TTYPORT 0x17755U 

       

    volatile char *port17 = (char)*TTYPORT;

     *port17 = 'o';

     *port17 = 'N';

  • Without the volatile modifier, the compiler would think that the statement *port17 = 'o'; is redundant and would remove it from the object code. The volatile statement prevents the compiler optimisation.Alex Arzon.