학습자료(~2017)/C,C++

register 변수와 volatile 변수

단세포소년 2011. 4. 5. 07:44
반응형

register 변수와 volatile 변수

== register 변수

변수는 보통 컴퓨터의 메모리에 저장된다. 그러나 운이 좋으면 레지스터 변수는 CPU 레지스터 또는 더 일반적으로 가장 빠른 메모리에 저장되는데, 보통 변수보다 더 빠르게 접근하고 계산될 수 있다. 이외의 부분들은 자동변수와 똑같다. 이때 운이 좋으면 이라고 표현한 것은 레지스터 변수의 수가 제한되기 때문이다. 컴파일러는 레지스터 수와 선언된 변수의 수를 가중치에 따라 조정하기 때문에 레지스터 변수로 선언했지만 자동 변수로 되는 경우도 있을 수 있다. 그리고 레지스터 변수로 선언될 수 있는 형도 제한이 있다. 또한 & 연산자도 레지스터 변수에는 적용할 수 없다.


== volatile 변수

변수를 register가 아닌 메모리에 저장한다. 아무리 많은 register 변수를 선언하더라도 CPU의 register 개수에는 한계가 있기 때문에 register 선언이 효과를 지니지 않고 메모리 변수로 되는 경우도 있다.

/* 레지스터 변수를 사용하는 예제 */

#include<stdio.h>

int jegob(int a, int b); /* 사용자 정의 함수를 선언 */

void main(void)
{
int a = 0, b = 0;

printf("원하는 수는 :");
scanf("%d", &a);
printf("몇 제곱을 원하십니까?");
scanf("%d", &b);

if (b <= 0)

return;

jegob(a, b); /* 사용자정의 함수를 호출 */
}


int jegob(int a, int b)
{
register int n; /* 레지스터 변수로 n을 선언 */

long c = 1;

for(n = 1; n <= b; n++)
{
c *= a;
printf("%d의%d제곱은 %d입니다.\n", a, n, c);
}
return; /* 빈 값으로 돌려줌 */
}


프로그램을 코딩하는 중에 자주 쓰는 변수들을 register 변수로 선언하여 사용한다. 하지만 똑똑한 Compiler는 직접 선언해 주지 않아도 register 변수로 잡기도 한다(Compiler에 의한 최적화 수행). Memory Mapped I/O(메모리의 특정 영역을 특정 장치와 연결해서 사용)의 경우에는 문제가 발생한다. 왜냐하면, 어떤 장치의 한 영역을 나타내는 변수가 있고 이 변수를 통하여 그 장치의 상황을 받아들여 특정한 일을 처리하는 경우 당연히 이 변수는 자주 쓰이는 변수가 되므로 Compiler에 의해서 Register 변수로 처리가 될 수 있는데 이를 미연에 방지하고자 volatile이라는 Keyword를 주게 되면 Compiler는 이 변수에 대한 최적화를 수행하지 않게 된다. 따라서, 원하는 결과를 얻을수 있게 된다.


메모리 변수(volatile variable)는 지역 변수를 선언할 때 레지스터가 아닌 반드시 메모리에 변수를 만들어 처리하도록 한다. 그러면 "할 수 있다면 빠른 레지스터 변수를 사용하는 것이 더 낫지 않느냐?" 라는 반론이 제기될 수 있는데 일반적으로 메모리 변수는 인터럽트 루틴 등의 변수에 사용된다. 레지스터 변수인 경우 인터럽트 루틴에서 그 변수의 내용을 바꾸면 레지스터에 있는 값이 바뀌므로 언제 그 레지스터의 값이 없어질지 알 수 없다. 이런 경우 메모리 변수로 선언하기 위해 volatile을 사용한다.


volatile은 컴파일러 키워드이다. 특별히 따로 최적화를 하지 않고 쓴대로 수행시키라는 의미이다. 함수에 이것이 있으면 함수 내부를 컴파일할때 최적화 옵션이 있어도 최적화를 수행 하지 않는다. 변수에 이것이 붙으면 속도를 위해서 변수를 레지스터에 올려놓고 쓰는일 같은 것을 안하게 된다. 보통 IO Port를 특별한 변수 이름으로 지정해놓고 사용하고자 할때 volatile키워드를 붙여서 쓴다.


일반적으로 컴파일러는 최적화를 하는 동안 불필요 하다고 생각되는 코드를 생략을 하게된다. 그러나 volatile을 사용하는 경우 이 변수와 관련된 내용은 생략하지 않는다. 키워드 volatile이 정의되어 있지 않으면 컴파일러는 마지막 명령만 남기고 이전 명령들을 생략할 수 있다.

예)

char * Addr;

Addr = (char *)0x10000;
*Addr = 0x55; /*생략한다..*/
*Addr = 0xAA; /* 이것만 인식한다*/


위의 경우 단지 0x10000에 0xAA만 쓴 것으로 컴파일러는 생각하게 된다. 일반적으로 같은 곳에 값을 써주거나 값을 읽을 일이 많은 메모리나 레지스터인 경우에는 volatile를 써주는 것이 좋다. 만약 컴파일러가 최적화 옵션을 사용하지 않는다면 써줄 필요는 없다.


volatile 선언은 디버그 모드에서는 작동하지 않고 보통 릴리즈 모드에서 최적화시에 발생한다. 쉬운 예를 들자면 for (;;) { int a = 1; .. .. } 이렇게 되어 있을때. 이 루프가 한 100만번 돈다고 하면. int a = 1;은 루프 밖으로 빼는게 실행 속도가 빨라진다. 컴파일러는 자기가 알아서 이런 것을 루프 밖으로 빼버린다. 그런데 이 문장을 빼는 것이 큰 문제가 되는 경우도 있다. 이럴때 volatile int a = 1 해주면 최적화 대상에서 제외 된다.


참고

Nesting: Sub-routine에서 Sub-routine을 부르는것(Call)을 의미한다.


먼저 다음 물음에 답해보자.


혹시 당신이 C로짠 임베디드 코드에서 다음과 같은 경우를 경험한 적이 있는가?


* 옴티마이즈 옵션을 켜기 전까지는 코드가 잘 동작한다.
* 어떤 인터럽트를 disable 시킨 동안에는 코드가 잘 동작한다.
* RTOS가 탑재된 멀티태스킹 시스템에서 어떤 태스크(TASK)가 enable 되기 전까지는 태스크가 잘 동작한다.


만약 위의 물음에 "네(yes)"라고 대답한다면 그것은 바로 당신이 volatile라는 C keyword를 사용하지 않았기 때문이다. 이건 비단 당신혼자만의 문제는 아니다. 많은 프로그래머들이 volatile 키워드에 대해서 어설프게 잘못 알고 있거나 제대로 사용하지 않고 있다. 이건 그리 놀랄만한 일이 아닌데 그건 바로 많은 C 책이 이점에 관해서 너무 하리만큼 무심하기 때문이다.


volatile은 변수를 선언할 때 같이 사용하는 키워드이다. volatile을 사용함으로 인해 컴파일러에게 volatile과 함께 선언된 변수는 언제 어느 때든지 값이 바뀔수 있다는 것을 말한다. 구체적인 사용 예를 들기 전에 먼저 volatile에 대한 문법적인 사항을 알아보자. volatile 변수를 사용하기 위해 volatile 키워드를 정의된 변수의 데이터 타입 앞 또는 뒤에 명시하면 된다. 다음과 같이 말이다.

volatile int foo;
int volatile foo;


자, 그럼 포인터에서는 어떻게 될까? 포인터에서 뭔가 특별한 점이 있다고 생각하는가? 포인터라 해서 별반 다를게 없다. 포인터 역시 다음과 같이 하면된다.


volatile int *foo;
int volatile *foo;


마지막으로 volatile을 struct나 union에 적용시켜버리면 struct나 union의 모든 내용들은 volatile이다. 만약 위와 같은 경우를 원하지 않는다면 어떻게 할것인가? struct나 union 멤버에게 개별적으로 사용하면 된다. 자, 그럼 본격적인 사용법을 알아보자.


어떤 변수들이 예고 없이 값이 바뀔 수 있을 가능성이 있는 경우에는 volatile로 선언해야 한다. 사실상 다음의 3가지 타입의 변수들이 바뀔 수 있다.


* memory-mapped periherral registers
* 인터럽트 서비스 루틴에 의해 수정되는 전역변수
* 멀티 태스킹 또는 멀티 쓰레드에서 사용되는 전역변수


그럼 먼저 첫번째 항목에 대해서 좀더 자세히 알아보자.


임베디드 시스템에서는 진보되고 복잡한 실질적인 주변 디바이스(peripheral)를 포함하게 된다. 이런 peripheral들은 프로그램 흐름과 비동기적으로 값들이 변하는 레지스터들을 가지고 있는 경우가 대부분이다. 매우 간단한 예로 0x1234 address에 위치한 8비트 status 레지스터가 있다고 가정하고 생각해보자. 만약 이 레지스터가 0이 아닌 값을 가질때까지 이 레지스터를 폴링(polling)한다고 가정해보자. 그럼 당신은 분명히 다음과 같이 코드를 작성할것이다.


INT8U *ptr = (INT8U *)0x1234; //wait for register to become non-zero
while (*ptr == 0); //Do something else


만약 당신이 옴티마이즈 옵션을 켰다면 위의 코드는 제대로 동작하지 않을 확률이 굉장히 높다. 왜냐하면 컴파일러는 당신이 작성한 코드에 대해서 다음과 같은 어셈블러를 생성할 것이다. 반드시 유심히 보길 바란다. 중요하다.


move ptr, #0x1234
move a, @ptr
loop bz loop


자, 한번 분석해보자. 컴파일러는 굉장히 똑똑하게 어셈블리 코드를 생성한 것을 볼 수 있다. 첨에 한번반 0x1234를 액세스해서 값을 로딩한 이후로 두번 다시는 0x1234를 억세스 하지 않는다. 두 번째 코드에서 볼 수 있듯이 값은 accumulator에 이미 로딩이 되있기 때문에 값을 재 로딩할 필요가 없다고 컴파일러는 판단하기 때문이다. 왜냐하면 값은 항상 같다고 보기 때문이다. 그러므로 3 번째 라인에 의해 당신이 작성한 코드는 무한 루프에 빠지게 된다. 정작 우리가 원하는 동작을 하기 위해서는 위의 코드를 다음과 같이 수정해야 한다.


INT8U volatile * ptr = (INT8U volatile *)0x1234;


그럼 컴파일러는 이제 다음과 같이 어셈블러를 생성할 것이다.


mov ptr, #0x1234
loop mov a, @ptr
bz loop


자, 어떤가? 드디어 당신이 원하는 결과를 얻게 될 것이다.


자, 그럼 인터럽트 서비스 루틴의 경우에 대해서 생각해보자. 종종 인터럽트 서비스 루틴은 메인함수에서 테스트하는 변수를 셋팅하게된다. 예를 들어 시리얼 포트 인터럽트는 각각에 수신한 캐릭터에 대해 ETX 캐릭터가 수신됐는지를 테스트한다고 가정해보자. 만약 ETX가 수신되면 인터럽트 서비스 루틴은 전역 플래그를 셋팅할 것이다. 불완전한 코드를 다음에 보이겠다.


int ETXRcvd = FALSE;


void main (void)
{
...
while (!ETXRcvd)

{
//what
}
...
}


interrupt void RxISR (void)
{
...


if (rx_char == ETX)

{
ETXRcvd = TRUE;
}

...
}


옵티마이즈 옵션을 꺼논 동안에는 코드가 올바르게 작동할 것이다. 그러나, 그렇지 않을 경우에는? 문제는 컴파일러는 ETXRcvd가 인터럽트 서비스 루틴에 의해서 값이 바꼈을 경우 이를 알 수 없는 경우가 생긴다. 위에 peripheral 예에서 들었듯이 !EXTRcvd는 항상 참이기 때문에 while 루프를 절대 벗어날 수 없는 경우가 생길 수도 있다. 게다가 심지어는 이런 이유로 인해 루프 이후에 코드들은 옵티마이즈에 의해 제거 되어버릴 수도 있다. 만약 당신이 운 좋은 놈이라면 당신의 컴파일러는 이런 문제에 대해서 경고 메세지를 보내게 될것이다. 그렇지 않고 당신이 운좋은 놈이 아니거나 컴파일러가 제공하는 경고 메세지가 때로는 얼마나 무서운 것인지를 경험해보지 못했다면 어떻게 될까? 말안해도 알리라 본다. 마지막으로 멀티 쓰레드또는 멀티 태스킹 어플리케이션 경우를 생각해 봐야 한다.


*((volatile byte *)(port))


port라는 포인터 변수가 가리키는 번지의 byte 형을 읽는다.

(port라는 포인터 변수를 byte 형을 가리키는 포인터로 전환한 후(cast 연산자) 이곳의 내용을 참조하라는 의미(* 연산자))


포인터 변수의 이름이 port인 것과 volatile이라는 키워드가 나온 것으로 보아 memory mapped io 루틴에 사용되었을 가능성이 있다. volatile이라는 키워드는 변수가 언제 어느 때든지 값이 바뀔 수 있기 때문에 값을 항상 메모리(레지스터가 아닌)에만 보관하라는 의미이다. 일반적인 프로그래밍에서는 보통 잘 쓰이진 않지만 하드웨어 관련 프로그래밍에서는 매우 중요한 키워드이다.


똑똑한 컴파일러들은 반복문 등에서 어떤 변수의 사용이 잦을 경우에 자기가 알아서 자동으로 해당 변수를 register 형으로 사용하는 경우가 있다. (변수가 위치한 메모리 주소를 직접 액세스 하는 것보다 레지스터에 넣고 사용하는 것이 적게는 수십 배에서 많게는 수백 배까지 빠르기 때문이다.) 이렇게 해당 변수를 레지스터에 넣고 사용하다가 반복문이 끝나면 메모리로 복귀시키는 경우가 있는데 문제는 프로그램이 수행되는 도중에 인터럽트에 의해서라던가 아니면 다른 쓰레드에 의해 해당 변수의 내용(메모리 상에 있는 내용)이 바뀌는 경우가 있다. 이 상황에서 그 변수를 register에 넣고 돌아가는 코드가 동시에 돌아가고 있다면 그 곳에서는 변수의 내용이 바뀌었는지 알아챌 수가 없다. 따라서 이로 인해 프로그램이 오작동하거나, 큰 문제를 일으킬 수 있다. 반대의 경우로서, 변수를 레지스터에 넣고 돌리는 동안에는 다른 루틴에서 이 변수의 내용이 바뀌었는지의 여부를 알 수가 없다.


이러한 상황을 방지하기 위해, 변수를 선언할 때, volatile라는 키워드를 사용하면 그 변수를 참조할 때 무조건 메모리 상의 내용을 바탕으로 하기 때문에 문제를 해결할 수가 있다.


또한 위에서 말했던 memory mapped io 등과 같은 곳에서도 해당 메모리 번지를 '직접' 액세스하여야만 하는 것이기 때문에 만약의 사태에 대비하여 volatile로 선언하곤 한다.

출처 : http://blog.naver.com/lovinghc

반응형