84. restrict 포인터
1. 키워드
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),%eax
는 mov
명령어로 (%rdx)
의 값을 %eax
로 복사한다는 뜻이다.
- 즉, C의 역참조
*x
를 어셈블리에서는 저렇게 표현한다.
- 그다음 줄의
add %eax,(%rdi)
는 add
명령어로 (%rdi)
에 %eax
의 값만큼 더한다는 뜻이다.
- 즉,
*a += %eax
가 된다.
- 마찬가지로
*b += *x;
도 mov (%rdx),%eax
와 add %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));
memcpy
는 restrict
가 붙어있고 memmove
는 restrict
가 없다.
- 여기서
memmove
는 내부적으로 같은 메모리 공간을 가리키는지, 메모리가 겹치는지 모두 확인을 하기 때문에 성능이 떨어진다.
- 만약 두 메모리 공간이 다른 공간을 가리키고 겹치지 않는다면 최적화된
memcpy
를 사용하여 성능을 향상시킬 수 있다.
References