Skip to content

84. restrict 포인터


1. 키워드

  • restrict 포인터


2. restrict 포인터

  • restrict 포인터는 메모리 접근에 관련된 최적화 기능이다.
  • 예를 들어 다음과 같이 포인터를 역참조하여 값을 증가시키는 코드가 있다.


void increase(int *a, int *b, int *x)
{
    *a += *x; // x를 역참조하여 가져온 값만큼 *a를 역참조하여 값을 증가시킴
    *b += *x; // x를 역참조하여 가져온 값만큼 *b를 역참조하여 값을 증가시킴
}


  • 이 코드를 GCC에서 컴파일한 뒤 어셈블리를 살펴보자.


gcc -g -std=c99 -O3 -c increase.c
objdump -S increase.o
void increase(int *a, int *b, int *x)
{
    *a += *x;
   0:   8b 02                   mov    (%rdx),%eax // x를 역참조하여 가져온 값을 eax에 저장
   2:   01 07                   add    %eax,(%rdi) // eax의 값만큼 a를 역참조하여 값을 증가시킴
    *b += *x;
   4:   8b 02                   mov    (%rdx),%eax // x를 역참조하여 가져온 값을 eax에 저장
   6:   01 06                   add    %eax,(%rsi) // eax의 값만큼 b를 역참조하여 값을 증가시킴
   8:   c3                      retq


  • %rdx는 세 번째 매개변수인데 (%rdx)처럼 ()(괄호)로 묶으면 %rdx에 저장된 메모리 주소로 접근한다는 뜻이다.
  • 그리고 mov (%rdx),%eaxmov 명령어로 (%rdx)의 값을 %eax로 복사한다는 뜻이다.
  • 즉, C의 역참조 *x를 어셈블리에서는 저렇게 표현한다.
  • 그다음 줄의 add %eax,(%rdi)add 명령어로 (%rdi)%eax의 값만큼 더한다는 뜻이다.
  • 즉, *a += %eax가 된다.
  • 마찬가지로 *b += *x;mov (%rdx),%eaxadd %eax,(%rsi)로 처리한다.


  • 어셈블리로 되어 있어서 좀 복잡하지만 핵심은 간단하다.
  • 다음과 같이 매개변수에 들어있는 메모리 주소 a, b, x가 같은 공간일 수도 있기 때문에 컴파일러는 매번 mov (%rdx),%eax와 같이 메모리에 접근하여 값을 가져온다.
  • 왜냐하면 같은 메모리 공간일 경우 이전 명령어의 결과가 확실히 적용되고 난 다음에 값을 가져와야 하기 때문이다.


int a = 1;
increase(&a, &a, &a); // &a, &a, &a는 같은 공간을 가리킴
// a, b, x는 같은 공간을 가리킴
void increase(int *a, int *b, int *x)
{
    *a += *x; // 같은 공간의 값을 꺼내서 증가시킴
    *b += *x; // x는 a와 같은 공간이므로
              // 이전의 연산 결과가 확실히 적용되고 난 다음에 값을 가져와야 함
}


  • 이처럼 같은 메모리 공간을 가리키는 포인터를 에일리어스(Alias)라고 부르는데, 메모리가 같은 공간에 접근하는지 확인하여 처리하고, 잘못 처리했을 경우 되돌리는 작업은 상당히 복잡하고 비용이 많이 든다.
  • 여기서는 값을 더하는 단순한 작업이지만 실제로는 어셈블리가 매우 복잡해진다.
  • 그래서 포인터 에일리어스가 아닌 상황에서는 좀 더 최적화를 하기 위해 restrict 포인터라는 기능이 나왔다.


  • 이제 이 restrict 포인터를 사용해 보자.


void increase(int *restrict a, int *restrict b, int *restrict x)
{
    *a += *x;
    *b += *x;
}


  • int *restrict a와 같이 *restrict를 붙이면 restrict 포인터가 된다.
  • 이 코드를 GCC에서 컴파일한 뒤 어셈블리를 살펴보자.


gcc -g -std=c99 -O3 -c increase.c
objdump -S increase.o
void increase(int *restrict a, int *restrict b, int *restrict x)
{
    *a += *x;
   0:   8b 02                   mov    (%rdx),%eax
   2:   01 07                   add    %eax,(%rdi)
    *b += *x;
   4:   01 06                   add    %eax,(%rsi)
   6:   c3                      retq


  • 잘 보면 *b += *x;의 어셈블리 명령 한 줄이 줄어든 것을 알 수 있다.
  • 컴파일러가 속도 향상을 위해서 최적화를 한 것이다.


  • restrict 포인터는 각 포인터가 서로 다른 메모리 공간을 가리키고 있고, 다른 곳에서 접근하지 않으니 컴파일러가 최적화를 하라는 뜻이다.
  • 여기서는 프로그래머가 알려준대로 a, b, x가 서로 다른 메모리 공간을 가리키고 있다고 보고 x를 역참조하여 값을 가져오는 mov (%rdx),%eax 명령을 한 번 줄이게 된다.
  • 즉, 다른 메모리 공간이므로 이전 명령어의 결과가 확실히 적용되는지는 알 필요없이 값을 그대로 사용한다.


int a = 1;
int b = 1;
int x = 1;
increase(&a, &b, &x); // &a, &b, &x는 다른 공간을 가리킴
// a, b, x는 다른 공간을 가리킴
void increase(int *restrict a, int *restrict b, int *restrict x)
{
    *a += *x; // 다른 공간의 값을 가져와서 증가시킴
    *b += *x; // x는 a와 다른 공간이므로
              // 이전의 연산 결과가 확실히 적용되는지는 알 필요 없이 값을 그대로 사용
}


  • restrict 포인터는 컴파일러에게 최적화를 하라고 알려주는 키워드이다.
  • 메모리가 다른 공간을 가리킨다고 보장하거나 메모리 공간을 검사하는 용도가 아니다.
  • 만약 같은 메모리 공간을 가리키는 포인터에 restrict를 붙여서 컴파일하게 되면 최적화 때문에 잘못된 결과가 나올 수 있으니 주의해야 한다.
  • 따라서 포인터가 가리키는 메모리 공간을 프로그래머가 직접 확인한 뒤 다른 공간을 가리킬 때만 restrict를 사용해야 한다.


  • C 표준 라이브러리에서는 restrict 포인터를 사용하여 최적화를 하고 있다.


extern void *memcpy (void *__restrict __dest, const void *__restrict __src,
                     size_t __n) __THROW __nonnull ((1, 2));

extern void *memmove (void *__dest, const void *__src, size_t __n)
                      __THROW __nonnull ((1, 2));


  • memcpyrestrict가 붙어있고 memmoverestrict가 없다.
  • 여기서 memmove는 내부적으로 같은 메모리 공간을 가리키는지, 메모리가 겹치는지 모두 확인을 하기 때문에 성능이 떨어진다.
  • 만약 두 메모리 공간이 다른 공간을 가리키고 겹치지 않는다면 최적화된 memcpy를 사용하여 성능을 향상시킬 수 있다.

References