본문 바로가기
Research/Programming

register와 volatile 키워드의 역할

by sunnyan 2002. 12. 4.
728x90
register와 volatile 키워드는 해당 변수의 메모리 적재와 관련되어 있으며, 어떤 의미에서는 상반된 역할을 수행하도록 컴파일러에 지시합니다.

register 키워드
일반적인 변수선언의 형태처럼 아래와 같이 선언했다면,

int i;

해당 변수가 전역 변수로 선언된 경우에는 프로그램의 데이터 영역에, 지역 변수로 선언된 경우에는 스택 영역에 변수의 위치가 할당됩니다. 지역 변수가 스택 영역에 할당되는건, 해당 언어의 스펙과 구현에 따라 다를 수 있습니다. 대개의 경우 재귀호출을 허용하는 언어인 경우에는 스택 영역에, 그렇지 않은 언어의 경우에는 데이터 영역에 지역 변수를 할당하게 됩니다. 조금 말이 길었지만 결국 메모리 어딘가에 전역 변수든 지역변수든 할당된다는 것입니다. 예를 들어 아래와 같은 코드가 있다고 가정해 봅시다.

int i, sum, limit;
...
for (i=0; i < limit; i++) {
  sum = sum + i;
}
...

앞에서 설명한 바와 같이 위의 코드에서는 i, sum, limit 변수 모두 메모리 어딘가에 할당됩니다. 여기서 메모리(변수) 관련 연산들을 살펴보면 i를 0으로 초기화, i와 limit을 비교, i에 1을 증가, sum을 i만큼 증가시키는 연산이 필요하게 됩니다. 해당 변수값이 메모리에 할당되어 유지되므로 필요한 메모리 연산을 계산해보면, 루프를 한 번 수행하는 동안 sum 변수는 읽기 1번, 쓰기 1번, i 변수는 읽기 3번, 쓰기 1번, limit 변수는 읽기 1번이 필요합니다. 모두 합해서 7 * limit 횟수 만큼의 메모리 읽고 쓰기가 필요하게 되겠습니다.

메모리 연산은 CPU의 여러 연산 중 상대적으로 느리게 동작하는 연산 중에 하나입니다. 그나마, 80년대 초까지는 CPU의 클럭 속도가 낮은 상태에서는 큰 부담이 없었지만 지금은 그 차이가 무척 크지요. 이러하 CPU와 메모리 사이의 속도의 현격한 차이가 CPU 내의 1차 캐쉬 그리고 외부에 2차 캐쉬 또는 3차 캐쉬까지 제공되는 이유가 된 것입니다. 결론적으로 위에서 설명한 바와 같이 모든 경우에 메모리에 있는 변수를 읽고 쓴다면 CPU가 메모리 읽기 및 쓰기 연산이 수행되는 동안 상당한 수준 대기해야 함으로 CPU 본래의 성능에 비해 프로그램이 느리게 동작하게 됩니다.

CPU 내에는 그 갯수가 한정적이지만, 메모리처럼 사용할 수 있는 메모리 공간이 있습니다. 바로 레지스터입니다. 어셈블리어 책을 보면, 해당 같은 연산이 레지스터를 이용한 경우와 메모리를 이용한 경우의 두가지로 나오는 경우가 있습니다. CPU의 종류에 따라서 메모리 참조방식에 따라 여러 개 나오기도 하지요. 이러한 동일연산의 수행시간을 살펴보면, 레지스터를 참조하는 경우가 메모리를 참조하는 경우보다 빠른 것을 알 수 있습니다.

이 정도면 어느 정도 짐작하겠지만, register 키워드는 프로그램의 최적화를 위해서 해당 변수를 가능한한 (레지스터 갯수가 한정되어 있으니까요) 메모리 대신 레지스터에 넣어서 사용하라는 얘기입니다. 위의 예에서 아래와 같이 코딩을 하였다면,

register int i, limit, sum;
...
for (i=0; i < limit; i++) {
  sum = sum + i;
}

for 루프를 수행하기 전에 Ra, Rb, Rc라는 레지스터에 각각 i, limit, sum의 값을 먼저 옮기고, 루프를 수행합니다. 물론 루프 수행중에는 메모리 대신 해당 레지스터를 참조하겠지요. 루프 수행이 마치고 나면, 다시 메모리 변수 i, limit, sum의 값을 레지스터 Ra, Rb, Rc에서 읽어 저장하게 됩니다. 그럼 모두합해서 읽기 3번, 쓰기 3번 총 6번의 메모리 접근만 일어나게 되겠지요. 당연히 빨라집니다.

물론 이런 수고를 사람이 직접 해야 하느냐? 그건 아닙니다. 앤만한 경우에는 컴파일러가 알아서 해주게 됩니다. 괜히 있는 레지스터를 놀릴만컴 컴파일러가 둔하지는 않습니다. 컴파일러가 판단하기에 자주 그리고 많이 사용되겠다는 변수를 우선순위를 매긴 다음, 현재 남아 있는 레지스터 가운데 적절히 할당합니다. 레지스터 갯수가 별루 많지도 않고 특정 연산에는 특정 레지스터를 사용해야 하는 x86 CPU는 이러한 레지스터 할당이 힘들겠지만 그래도 최소 SI, DI 레지스터 일반 용도로 사용할 수 있습니다. 최신 x86 CPU에서는 레지스터 수가 좀 늘었는지 모르겠군요. 대부분의 RISC CPU는 수개에서 수십개까지의 일반 목적으로 활용할 수 있는 레지스터를 보유하고 있으니, 적절히 레지스터에 변수를 할당하게 되면 무척 빨라지겠죠. 이 때문에 RISC CPU에서는 컴파일러의 성능이 특히나 중요하게 여겨지게 됩니다. 물론 사용자가 register 키워드를 통해서 직접 제어한다면, 컴파일러에 의한 변수의 레지스터 할당에 비해 우선적으로 처리되겠습니다.

정말 똑똑한 컴파일러라면 위의 코드를 아래와 같이 바꾸겠지요.

int i, limit, sum;
...
sum = (1 + limit) * limit / 2;
i = limit;

volatile 키워드
volatile의 경우 어떤 의미에서는 앞에서 설명한 컴파일러의 최적화와 관계있습니다. 그 외에도 CPU 내, 외부의 캐쉬와 갈은 하드웨어적인 최적화와도 관계가 있습니다.

volatile 키워드가 가장 많이 사용되는 경우의 하나가 memory-mapped I/O인 경우입니다. 메모리의 특정 영역을 특정 장치와 연결하여 사용하는 방법입니다. 가장 흔한 예가 비디오 메모리가 되겠고, 그 이외에도 많은 장치들을 이러한 식으로 사용될 수 있습니다.

즉, 자신 엄밀하게 말한다면 컴파일러가 컴파일하고 있는 코드의 상황과는 관계 없이 바뀔 수 있는 메모리 변수가 있다면, 해당 변수에 대해 특별한 최적화를 하지 못하도록 컴파일러를 제약하는 키워드가 volatile입니다.

예를 들어, 0x0C000000 번지에 특별한 장치가 있다고 하겠습니다. 이 장치가 일종의 센서라고 하고 입력되는 센서값의 범위에 따라 다른 동작을 수행하게 프로그래밍을 아래와 같이 하였다고 하면,

int *p = 0x0C000000;
while (1) {
  if (*p == 0) {
    break;
  } else if  (*p < lower_limit) {
    action 1;
  } else if (*p < upper_limit) {
    action 2;
  }
  wait 10 mili-seconds
}

똑똑한 컴파일러는 위의 코드를 아래와 아래와 같이 바꿉니다.

int *p = 0x0C000000;
register int pvar = *p;
if (pvar == 0) {
} else if (pvar < lower_limit) {
    while (1) {
        action 1;
        wait 10 mili-seconds;
    }
  } else if (pvar < upper_limit) {
    while (1) {
        action 2;
        wait 10 mili-seconds;
    }
}

위와 같이 코드가 변형된다면, 프로그래머가 의도한 바와는 다른 결과가 나타나게 됩니다. 이러한 최적화를 억제하는 목적으로 volatile을 사용합니다. 컴파일러는 volatile 키워드가 선언된 변수에 대해서는 무조건 메모리에 접근하여 됩니다. 이러한 memory-mapped I/O 이외에도, 쓰레드 등으로 프로그램을 만들어서 공유변수를 한쪽에서 읽고, 한쪽에서 쓰는 경우도 해당될 수 있으며, 시스템 시간과 같이 특정 위치의 변수값이 자신과는 독립적으로 계속 변하는 경우에도 사용할 수 있습니다.

또는 CPU 내 외부의 캐쉬에 의해서도 이러한 최적화 효과가 나타날 수 있습니다. 캐쉬내에 해당 메모리 번지 값이 저장되어 읽을 때마다 같은 값이 읽히고, 또한 적을 때도 실제 메모리에 저장되지 않고 캐쉬에 임시 보관될 수 있습니다. 다른 경우는 잘 모르겠지만 MIPS의 R 시리즈 CPU에서는 (아마 2000 인가 3000 시리즈로 기억됩니다) 메모리를 두가지 방법으로 접근할 수 있습니다. 메모리 주소의 최상위 비트가 0이면 캐쉬를 거친 일반적인 접근을, 최상위 비트가 1이면 똑같은 주소의 메모리를 캐쉬를 거치지 않고 직접 접근할 수 있습니다. 당연히 이런 컴퓨터의 컴파일러에서 volatile로 선언하면 0C000000이 아니라 8C000000의 메모리를 접근하게 됩니다.
728x90

'Research > Programming' 카테고리의 다른 글

#, ##  (0) 2002.12.04
C Bit Fields  (0) 2002.12.04
__cdecl을 사용하는 이유 ?  (0) 2002.12.04
volatile  (0) 2002.12.04
What Is Alignment  (0) 2002.12.04