restrict 타입 한정자
restrict 타입 한정자 (restrict type qualifier, since C99)
C의 타입 시스템에서는 const, volatile, 그리고 객체를 가리키는 포인터의 restrict라는 세 가지 한정자를 타입에 붙일 수 있어요. 이 페이지에서 그중 restrict 한정자가 무엇을 뜻하고, 언제 어떻게 쓰는지 살펴볼게요.
출처: cppreference
본문
C 타입 시스템의 각 개별 타입에는 그 타입의 한정된 버전이 여러 개 존재해요. 그것은 const, volatile, 그리고 (객체를 가리키는 포인터의 경우) restrict 한정자 중 하나, 둘, 또는 셋 모두에 대응해요. 이 페이지는 restrict 한정자의 효과를 설명해요.
객체 타입을 가리키는 포인터 또는 (가능하면 다차원인) 객체 타입의 배열에 대한 포인터(since C23)만 restrict로 한정될 수 있어요. 특히 다음은 오류예요:
int restrict * p
float (* restrict f9)(void)
restrict 의미론은 lvalue 표현식에만 적용돼요. 예를 들어, restrict-한정 포인터로의 캐스트나 restrict-한정 포인터를 반환하는 함수 호출은 lvalue가 아니므로 한정자가 효과가 없어요.
restrict-한정 포인터 P가 선언된 블록을 매번 실행할 때(보통 P가 함수 매개변수인 함수 본문을 매번 실행할 때), 만약 P를 통해 (직접적으로든 간접적으로든) 접근 가능한 어떤 객체가 어떤 방법으로든 수정된다면, 그 블록에서 그 객체에 대한 모든 접근(읽기와 쓰기 모두)은 반드시 P를 통해 (직접적이든 간접적이든) 일어나야 해요. 그렇지 않으면 동작이 정의되지 않아요:
void f(int n, int * restrict p, int * restrict q)
{
while (n-- > 0)
*p++ = *q++; // none of the objects modified through *p is the same
// as any of the objects read through *q
// compiler free to optimize, vectorize, page map, etc.
}
void g(void)
{
extern int d[100];
f(50, d + 50, d); // OK
f(50, d + 1, d); // Undefined behavior: d[1] is accessed through both p and q in f
}
객체가 절대 수정되지 않는다면, 그것은 앨리어싱될 수 있고 다른 restrict-한정 포인터들을 통해 접근될 수 있어요. (단, 앨리어싱된 restrict-한정 포인터들이 가리키는 객체들이 다시 포인터라면, 이 앨리어싱이 최적화를 방해할 수 있어요.)
한 restrict-한정 포인터에서 다른 restrict-한정 포인터로의 할당은 정의되지 않은 동작이에요. 단, 어떤 바깥 블록의 객체를 가리키는 포인터에서 안쪽 블록의 포인터로 할당하는 경우(restrict-한정 매개변수를 가진 함수를 호출할 때 restrict-한정 포인터 인자를 사용하는 것 포함)나, 함수에서 반환하는 경우(또는 from-pointer의 블록이 끝난 경우)는 예외예요:
int* restrict p1 = &a;
int* restrict p2 = &b;
p1 = p2; // undefined behavior
restrict-한정 포인터는 restrict-한정되지 않은 포인터에 자유롭게 할당할 수 있어요. 컴파일러가 코드를 분석할 수 있는 한 최적화 여지는 그대로 유지돼요:
void f(int n, float * restrict r, float * restrict s)
{
float *p = r, *q = s; // OK
while (n-- > 0)
*p++ = *q++; // almost certainly optimized just like *r++ = *s++
}
배열 타입이 restrict 타입 한정자로 선언되면(typedef를 통해), 배열 타입 자체는 restrict-한정되지 않고 그 원소 타입이 한정돼요. (until C23)
배열 타입과 그 원소 타입은 항상 동일하게 restrict-한정된 것으로 간주돼요. (since C23)
typedef int *array_t[10];
restrict array_t a; // the type of a is int *restrict[10]
// Notes: clang and icc reject this on the grounds that array_t is not a pointer type
void *unqual_ptr = &a; // OK until C23; error since C23
// Notes: clang applies the rule in C++/C23 even in C89-C17 modes
함수 선언에서, restrict 키워드는 함수 매개변수의 배열 타입을 선언하는 데 쓰이는 대괄호 안에 나타날 수 있어요. 그것은 배열 타입이 변환되는 포인터 타입을 한정해요:
void f(int m, int n, float a[restrict m][n], float b[restrict m][n]);
void g12(int n, float (*p)[n])
{
f(10, n, p, p+10); // OK
f(20, n, p, p+10); // possibly undefined behavior (depending on what f does)
}
참고 사항 (Notes)
restrict 한정자의 의도된 용도는 (register 저장 클래스처럼) 최적화를 촉진하는 것이고, 적합한(conforming) 프로그램을 구성하는 모든 전처리 변환 단위에서 한정자의 모든 인스턴스를 삭제해도 그 의미(즉 관찰 가능한 동작)는 바뀌지 않아요.
컴파일러는 restrict 사용의 앨리어싱 함의를 일부 또는 전부 무시할 자유가 있어요.
정의되지 않은 동작을 피하려면, 프로그래머는 restrict-한정 포인터가 주장하는 앨리어싱 단언이 위반되지 않도록 보장해야 해요.
많은 컴파일러는 언어 확장으로 restrict의 반대를 제공해요: 타입이 달라도 포인터들이 앨리어싱할 수 있음을 나타내는 속성인 may_alias(gcc)예요.
사용 패턴 (Usage patterns)
restrict-한정 포인터에는 몇 가지 흔한 사용 패턴이 있어요.
파일 스코프 (File scope)
파일 스코프의 restrict-한정 포인터는 프로그램이 실행되는 동안 단일 배열 객체를 가리켜야 해요. 그 배열 객체는 restrict-한정 포인터를 통해서와, 선언된 이름(있을 경우) 또는 다른 restrict-한정 포인터를 통해서 동시에 참조되어서는 안 돼요.
파일 스코프 restrict-한정 포인터는 동적으로 할당된 전역 배열에 접근을 제공하는 데 유용해요. restrict 의미론 덕분에 이 포인터를 통한 참조를, 정적 배열을 선언된 이름으로 참조하는 것만큼 효과적으로 최적화할 수 있어요:
float *restrict a, *restrict b;
float c[100];
int init(int n)
{
float * t = malloc(2*n*sizeof(float));
a = t; // a refers to 1st half
b = t + n; // b refers to 2nd half
}
// compiler can deduce from the restrict qualifiers that
// there is no potential aliasing among the names a, b, and c
함수 매개변수 (Function parameter)
restrict-한정 포인터의 가장 인기 있는 사용 사례는 함수 매개변수로 쓰는 것이에요.
다음 예제에서, 컴파일러는 수정된 객체들의 앨리어싱이 없다고 추론할 수 있어서 루프를 공격적으로 최적화할 수 있어요.
f에 진입할 때, restrict-한정 포인터 a는 그와 연관된 배열에 대한 배타적 접근을 제공해야 해요. 특히 f 안에서 b도 c도 a와 연관된 배열을 가리킬 수 없어요. 왜냐하면 둘 다 a에 기반한 포인터 값을 할당받지 않기 때문이에요. b의 경우는 선언의 const 한정자에서 명백하지만, c의 경우는 f의 본문을 조사해야 해요:
float x[100];
float *c;
void f(int n, float * restrict a, float * const b)
{
int i;
for ( i=0; i<n; i++ )
a[i] = b[i] + c[i];
}
void g3(void)
{
float d[100], e[100];
c = x; f(100, d, e); // OK
f( 50, d, d+50); // OK
f( 99, d+1, d); // undefined behavior
c = d; f( 99, d+1, e); // undefined behavior
f( 99, e, d+1); // OK
}
c가 b와 연관된 배열을 가리키는 것은 허용된다는 점을 주의하세요. 또한, 이러한 목적에서 어떤 포인터와 '연관된 배열'은 그 포인터를 통해 실제로 참조되는 배열 객체의 일부만을 의미한다는 점을 주의하세요.
위 예제에서, 컴파일러는 b의 const-성질이 보장하므로 b가 함수 본문에서 a에 의존할 수 없다는 것을 알고 a와 b가 앨리어싱하지 않는다고 추론할 수 있어요. 동등하게, 프로그래머는 void f(int n, float * a, float const * restrict b)라고 쓸 수도 있는데, 그 경우 컴파일러는 b를 통해 참조되는 객체들은 수정될 수 없다고 추론하고, 따라서 어떤 수정된 객체도 b와 a 둘 다로 참조될 수 없다고 추론해요. 프로그래머가 void f(int n, float * restrict a, float * b)라고 쓴다면, 컴파일러는 함수 본문을 조사하지 않고는 a와 b의 비-앨리어싱을 추론할 수 없어요.
일반적으로, 함수의 프로토타입에서 non-앨리어싱 포인터를 모두 restrict로 명시적으로 표기하는 것이 가장 좋아요.
블록 스코프 (Block scope)
블록 스코프의 restrict-한정 포인터는 자신의 블록으로 제한된 앨리어싱 단언을 만들어요. 타이트한 루프 같은 중요한 블록에만 적용되는 국소적 단언을 할 수 있게 해주고, restrict-한정 포인터를 받는 함수를 매크로로 변환하는 것도 가능하게 해줘요:
float x[100];
float *c;
#define f3(N, A, B) \
do \
{ int n = (N); \
float * restrict a = (A); \
float * const b = (B); \
int i; \
for ( i=0; i<n; i++ ) \
a[i] = b[i] + c[i]; \
} while(0)
구조체 멤버 (Struct members)
구조체의 멤버인 restrict-한정 포인터가 만드는 앨리어싱 단언의 범위는, 그 구조체에 접근하는 데 사용되는 식별자의 스코프예요.
구조체가 파일 스코프에 선언됐어도, 구조체에 접근하는 데 사용되는 식별자가 블록 스코프를 가지면 구조체의 앨리어싱 단언도 블록 스코프를 가져요. 앨리어싱 단언은 이 구조체 타입의 객체가 어떻게 생성됐느냐에 따라, 블록 실행 또는 함수 호출 동안에만 효력이 있어요:
struct t // Restricted pointers assert that
{
int n; // members point to disjoint storage.
float * restrict p;
float * restrict q;
};
void ff(struct t r, struct t s)
{
struct t u;
// r,s,u have block scope
// r.p, r.q, s.p, s.q, u.p, u.q should all point to
// disjoint storage during each execution of ff.
// ...
}
키워드 (Keywords)
restrict
예제 (Example)
코드 생성 예제예요. -S(gcc, clang 등) 또는 /FA(visual studio)로 컴파일해 보세요.
int foo(int *a, int *b)
{
*a = 5;
*b = 6;
return *a + *b;
}
int rfoo(int *restrict a, int *restrict b)
{
*a = 5;
*b = 6;
return *a + *b;
}
가능한 출력:
; generated code on 64bit Intel platform:
foo:
movl $5, (%rdi) ; store 5 in *a
movl $6, (%rsi) ; store 6 in *b
movl (%rdi), %eax ; read back from *a in case previous store modified it
addl $6, %eax ; add 6 to the value read from *a
ret
rfoo:
movl $11, %eax ; the result is 11, a compile-time constant
movl $5, (%rdi) ; store 5 in *a
movl $6, (%rsi) ; store 6 in *b
ret