Namespaces
Variants

restrict type qualifier (since C99)

From cppreference.net

C 타입 시스템 의 각 개별 타입에는 해당 타입의 여러 한정된(qualified) 버전이 존재하며, 이는 const , volatile , 그리고 객체 타입에 대한 포인터의 경우 restrict 한정자 중 하나, 둘, 또는 모두 세 가지에 해당합니다. 이 페이지는 restrict 한정자의 효과에 대해 설명합니다.

오직 객체 타입 또는 (다차원일 수 있는) 그 배열에 대한 포인터만 (C23부터) restrict 한정자를 가질 수 있습니다; 특히 다음은 오류입니다 :

  • int restrict * p
  • float ( * restrict f9 ) ( void )

restrict 의미론은 좌측값 표현식에만 적용됩니다. 예를 들어, restrict 한정 포인터로의 캐스트 또는 restrict 한정 포인터를 반환하는 함수 호출은 좌측값이 아니므로 한정자가 효과를 발휘하지 않습니다.

제한된 포인터 P 가 선언된 블록(일반적으로 P 가 함수 매개변수인 함수 본문)의 각 실행 동안, P 를 통해(직접 또는 간접적으로) 접근 가능한 어떤 객체가 수정되면, 해당 블록에서 그 객체에 대한 모든 접근(읽기 및 쓰기)은 P 를 통해(직접 또는 간접적으로) 발생해야 하며, 그렇지 않을 경우 동작은 정의되지 않습니다:

void f(int n, int * restrict p, int * restrict q)
{
    while (n-- > 0)
        *p++ = *q++; // *p를 통해 수정되는 객체들은 *q를 통해 읽히는 객체들과 동일하지 않음
                     // 컴파일러가 최적화, 벡터화, 페이지 매핑 등을 자유롭게 수행할 수 있음
}
void g(void)
{
    extern int d[100];
    f(50, d + 50, d); // 정상
    f(50, d + 1, d);  // 정의되지 않은 동작: d[1]이 f 함수 내에서 p와 q를 통해 모두 접근됨
}

객체가 수정되지 않는 경우, 이는 별칭이 지정되어 서로 다른 restrict 한정 포인터를 통해 접근될 수 있습니다 (별칭이 지정된 restrict 한정 포인터가 가리키는 객체들이 차례로 포인터인 경우, 이 별칭 지정이 최적화를 저해할 수 있음에 유의하세요).

한 제한된 포인터에서 다른 제한된 포인터로의 할당은 정의되지 않은 동작입니다. 단, 외부 블록에 있는 객체에 대한 포인터에서 내부 블록에 있는 포인터로 할당할 때(제한된 포인터 매개변수를 가진 함수를 호출할 때 제한된 포인터 인수를 사용하는 경우 포함) 또는 함수에서 반환할 때(그 외에도 from-포인터의 블록이 종료된 경우)는 예외입니다:

int* restrict p1 = &a;
int* restrict p2 = &b;
p1 = p2; // 정의되지 않은 동작

제한된 포인터는 제한되지 않은 포인터에 자유롭게 할당될 수 있으며, 컴파일러가 코드를 분석할 수 있는 한 최적화 기회는 유지됩니다:

void f(int n, float * restrict r, float * restrict s)
{
    float *p = r, *q = s; // 정상
    while (n-- > 0)
        *p++ = *q++; // 거의 확실하게 *r++ = *s++와 동일하게 최적화됨
}

배열 타입이 restrict 타입 한정자로 선언되면( typedef 사용을 통해), 배열 타입 자체는 restrict 한정이 아니지만 그 요소 타입은 restrict 한정입니다:

(C23까지)

배열 타입과 그 요소 타입은 항상 동일하게 restrict 한정된 것으로 간주됩니다:

(C23부터)
typedef int *array_t[10];
restrict array_t a; // a의 타입은 int *restrict[10]입니다
// 참고: clang과 icc는 array_t가 포인터 타입이 아니라는 이유로 이를 거부합니다
void *unqual_ptr = &a; // C23까지는 유효함; C23 이후부터는 오류
// 참고: clang은 C89-C17 모드에서도 C++/C23 규칙을 적용합니다

함수 선언에서, 키워드 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); // 정상
   f(20, n, p, p+10); // 정의되지 않은 동작 가능성 (f의 동작에 따라 다름)
}

목차

참고 사항

restrict 한정자(register 저장 부류와 마찬가지로)의 의도된 사용은 최적화를 촉진하는 것이며, 준수 프로그램을 구성하는 모든 전처리 번역 단위에서 이 한정자의 모든 인스턴스를 삭제해도 그 의미(즉, 관찰 가능한 동작)는 변경되지 않습니다.

컴파일러는 restrict 사용으로 인한 별칭 지정(aliasing)의 영향을 일부 또는 전부 무시할 자유가 있습니다.

정의되지 않은 동작을 피하기 위해, 프로그래머는 restrict 한정 포인터가 가정하는 별칭 지정(aliasing) 단언이 위반되지 않도록 보장해야 합니다.

많은 컴파일러는 언어 확장으로서 restrict 의 반대 기능을 제공합니다: 포인터의 타입이 달라도 서로 별칭(alias)이 될 수 있음을 나타내는 속성으로, may_alias (gcc) 등이 있습니다.

사용 패턴

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는 첫 번째 절반을 참조합니다
   b = t + n;  // b는 두 번째 절반을 참조합니다
}
// 컴파일러는 restrict 한정자로부터 a, b, c 이름들 사이에
// 잠재적인 별칭 사용이 없음을 추론할 수 있습니다

함수 매개변수

restrict 한정 포인터의 가장 일반적인 사용 사례는 함수 매개변수로 사용하는 것입니다.

다음 예제에서 컴파일러는 수정된 객체에 대한 별칭 지정(aliasing)이 없다고 추론하여 루프를 공격적으로 최적화할 수 있습니다. 함수 f 에 진입할 때, 제한된 포인터(restricted pointer) 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); // 정상
           f( 50,   d, d+50); // 정상
           f( 99, d+1,    d); // 정의되지 않은 동작
    c = d; f( 99, d+1,    e); // 정의되지 않은 동작
           f( 99,   e,  d+1); // 정상
}

c가 b와 연관된 배열을 가리키는 것이 허용된다는 점에 유의하십시오. 또한 이러한 목적을 위해, 특정 포인터와 연관된 "배열"이란 해당 포인터를 통해 실제로 참조되는 배열 객체의 일부만을 의미한다는 점도 유의하십시오.

위 예제에서 컴파일러는 b의 constness가 함수 본문 내에서 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-aliasing 포인터를 명시적으로 restrict 로 주석 처리하는 것이 가장 좋습니다.

블록 범위

블록 범위 restrict 한정 포인터는 해당 블록으로 제한된 별칭 지정 어서션을 만듭니다. 이를 통해 tight loop와 같은 중요한 블록에만 적용되는 지역적 어서션이 가능해집니다. 또한 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)

구조체 멤버

restrict 한정자 포인터가 구조체의 멤버일 때 이루어지는 별칭 어서션(aliasing assertion)의 범위는 해당 구조체에 접근하는 데 사용된 식별자(identifier)의 범위와 동일합니다.

구조체가 파일 범위에서 선언되더라도, 구조체에 접근하는 데 사용된 식별자가 블록 범위를 가질 때, 구조체 내의 별칭 어설션도 블록 범위를 가집니다; 별칭 어설션은 이 구조체 타입의 객체가 생성된 방식에 따라 블록 실행 내부나 함수 호출 내에서만 효과를 발휘합니다:

struct t      // 제한 포인터는 멤버들이 서로 분리된 저장 공간을
{
   int n;     // 가리킨다는 것을 단언합니다.
   float * restrict p;
   float * restrict q;
};
void ff(struct t r, struct t s)
{
   struct t u;
   // r,s,u는 블록 범위를 가짐
   // r.p, r.q, s.p, s.q, u.p, u.q는 모두 ff의 각 실행 동안
   // 서로 분리된 저장 공간을 가리켜야 함
   // ...
}

키워드

restrict

예제

코드 생성 예시; -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;
}

가능한 출력:

; 64비트 Intel 플랫폼에서 생성된 코드:
foo:
    movl    $5, (%rdi)    ; *a에 5 저장
    movl    $6, (%rsi)    ; *b에 6 저장
    movl    (%rdi), %eax  ; 이전 저장 작업이 *a를 수정했을 경우를 대비해 *a에서 다시 읽음
    addl    $6, %eax      ; *a에서 읽은 값에 6을 더함
    ret
rfoo:
    movl      $11, %eax   ; 결과는 11, 컴파일 타임 상수
    movl      $5, (%rdi)  ; *a에 5 저장
    movl      $6, (%rsi)  ; *b에 6 저장
    ret

참고문헌

  • C23 표준 (ISO/IEC 9899:2024):
  • 6.7.3.1 restrict의 형식적 정의 (p: TBD)
  • C17 표준 (ISO/IEC 9899:2018):
  • 6.7.3.1 restrict의 형식적 정의 (p: 89-90)
  • C11 표준 (ISO/IEC 9899:2011):
  • 6.7.3.1 restrict의 형식적 정의 (p: 123-125)
  • C99 표준 (ISO/IEC 9899:1999):
  • 6.7.3.1 restrict의 형식적 정의 (p: 110-112)