"일꾼이 일을 잘하려면 먼저 도구를 갈고 닦아야 한다." - 공자, 『논어』.
첫 장 > 프로그램 작성 > C의 모호한 "restrict" 키워드

C의 모호한 "restrict" 키워드

2024-11-04에 게시됨
검색:630

The Obscure “restrict” Keyword in C

소개

무엇보다도 C99는 프로그래머가 포인터가 범위 내 주어진 객체에 대한 전용 포인터임을 지정하고 결과적으로 컴파일러에 "힌트"를 제공하는 방법으로 제한 키워드를 추가했습니다. ” 해당 포인터를 통해 객체에 액세스할 때 추가 최적화를 수행할 수 있습니다.

문제

restrict가 해결하려는 문제를 설명하려면 다음과 같은 함수를 고려하세요.

void update_ptrs( int *p, int *q, int const *v ) {
  *p  = *v;
  *q  = *v;
}

컴파일러는 다음과 같은 x86-64 코드를 생성합니다.

mov eax, [rdx]  ; tmp = *v   // 1
add [rdi], eax  ; *p  = tmp
mov eax, [rdx]  ; tmp = *v   // 3
add [rsi], eax  ; *q  = tmp

라인 1과 중복되는 것처럼 보이는 라인 3을 생성하는 이유가 궁금할 것입니다. 문제는 컴파일러가 다음과 같은 작업을 수행하지 않았다는 것을 알 수 없다는 것입니다.

int x = 1, v = 2;
update_ptrs( &v, &x, &v );   // x = 5, v = 4

update_ptrs()에서 p와 v는 동일 int를 별칭하므로 컴파일러는 안전하게 작동해야 하며 *v의 값이 읽기 사이에 변경될 수 있다고 가정해야 합니다. 따라서 추가 mov 명령이 필요합니다.

일반적으로 C의 포인터는 최적화를 혼란스럽게 합니다. 컴파일러는 두 포인터가 서로 별칭인지 여부를 알 수 없기 때문입니다. 성능이 중요한 코드에서 메모리 읽기를 제거하는 것은 할 수 큰 승리가 될 수 있습니다. 만약 컴파일러가 이를 안전하게 수행할 수 있습니다.

해결책

앞서 언급한 문제를 해결하기 위해 주어진 포인터가 포인터 범위에 있는 객체에 대한 유일 포인터임을 지정할 수 있도록 제한이 C에 추가되었습니다. 즉, 동일한 범위 별칭의 다른 포인터는 없습니다. 그것.

restrict를 사용하려면 선언에서 *와 포인터 이름 사이에 를 삽입합니다. 제한을 사용하기 위해 다시 작성된 update_ptrs()는 다음과 같습니다:

void update_ptrs_v2( int *restrict p, int *restrict q, int const *restrict v ) { *p = *v; *q = *v; }
void update_ptrs_v2( int *restrict p, int *restrict q,
                     int const *restrict v ) {
  *p  = *v;
  *q  = *v;
}
(오른쪽에서 왼쪽으로 읽습니다. 예를 들어 v는 상수 int에 대한 제한된 포인터입니다. 또는 cdecl을 사용합니다.)

제한을 추가함으로써 이제 컴파일러는 다음과 같은 코드를 생성할 수 있습니다.


mov eax, [rdx] ; tmp = *v 추가 [rdi], eax ; *p = tmp 추가 [rsi], eax ; *q = 시간
void update_ptrs_v2( int *restrict p, int *restrict q,
                     int const *restrict v ) {
  *p  = *v;
  *q  = *v;
}
이제 컴파일러는 추가 mov 명령의 이전 라인 3을 제거할 수 있었습니다.

restrict가 사용되는 가장 잘 알려진 예는 아마도 표준 라이브러리 함수 memcpy()입니다. 소스 주소와 대상 주소가

겹치지 않는 경우 메모리 덩어리를 복사하는 가장 빠른 방법입니다. 약간 느린 memmove() 함수는 주소 do가 겹칠 때 사용하기 위해 존재합니다. 함정

restrict를 잘못 사용하면 정의되지 않은 동작이 발생합니다. 예를 들어

do

서로 별칭을 지정하는 포인터를 update_ptrs_v2() 또는 memcpy()에 전달합니다. 일부 경우에 컴파일러는 경고할 수 있지만 모든 경우에 그런 것은 아니므로 잘못된 사용을 잡기 위해 컴파일러에 의존하지 마십시오. 제한은 특정 범위에 대한 것입니다. 하나의 제한된 포인터를

동일 범위

의 다른 포인터에 할당하면 다음과 같은 정의되지 않은 동작이 발생합니다.
void f( int *restrict d, int *restrict s ) { int *restrict p = s; // 정의되지 않은 동작

void f( int *restrict d, int *restrict s ) {
  int *restrict p = s;    // undefined behavior


void f( int *restrict d, int *restrict s ) { int *p = s; // 좋아요

void f( int *restrict d, int *restrict s ) {
  int *p = s;             // OK

내부 범위의 제한된 포인터를 외부 범위의 다른 포인터에 할당하는 것도 괜찮습니다(그러나 그 반대는 안 됨):


void f( int *restrict d, int *restrict s ) { { // 내부 범위 int *restrict p = s; // 좋아요 // ... s = p; // 정의되지 않은 동작 } }

void f( int *restrict d, int *restrict s ) {
  {                       // inner scope
    int *restrict p = s;  // OK
    // ...
    s = p;                // undefined behavior
  }
}

먼저, 확실히 코드를 프로파일링하고(아마도 생성된 어셈블리 코드를 살펴보며) 제한을 사용하면 실제로

상당히

성능이 향상되어 잠재적 위험을 감수할 수 있는지 확인해야 합니다. 제한을 잘못 사용하여 발생한 버그를 진단하는 것은 매우 어렵습니다. 둘째, 제한 사용이 제한된 포인터를 통해 액세스된 메모리가

당신

에 의해 할당된 함수의 구현으로 제한된다면 더 안전합니다. 예를 들어, 다음과 같습니다:
무효 안전( unsigned n ) { n = n % 2 != 0; // 반올림하여 짝수로 만듭니다. int *const array = malloc(n * sizeof(unsigned) ); 부호 없음 *제한 half_1st = 배열; unsigned *restrict half_2nd = 배열 ​​n/2; // ... 무료(배열); }

void safer( unsigned n ) {
  n  = n % 2 != 0;  // make even by rounding up
  int *const array = malloc( n * sizeof(unsigned) );
  unsigned *restrict half_1st = array;
  unsigned *restrict half_2nd = array   n/2;
  // ...
  free( array );
}

셋째, 제한이 함수의 매개변수에 사용되면 잠재적으로

안전합니다. 예를 들어 safer()는 호출자가 포인터를 제어하는 ​​update_ptrs_v2()와 대조됩니다. 당신이 알고 있는 모든 것에 대해 발신자는 그것을 잘못 알아차리고 별칭을 붙인 포인터를 전달했습니다. 여러 가지 잡다한

객체(또는 void)에 대한

포인터만 제한:으로 한정할 수 있습니다.
int x 제한; // 오류: 개체를 제한할 수 없습니다. int 제한 *p; // 오류: 객체를 제한하기 위한 포인터 int (*f 제한)(); // 오류: 함수 포인터

restrict int x;       // error: can't restrict object
int restrict *p;      // error: pointer to restrict object
int (*restrict f)();  // error: pointer-to-function


구조체 노드 { 무효 *데이터 제한; 구조체 노드 *왼쪽 제한; 구조체 노드 *권한 제한; };

struct node {
   void *restrict data;
   struct node *restrict left;
   struct node *restrict right;
};

마지막으로 C에는 제한이

없습니다

. 왜 안 돼? 답변이 길지만 TL;DR 버전은 다음과 같습니다.

C 위원회가 C에서 가져오기를 원하지 않는 찾기 어려운 버그의 소스가 될 수 있습니다.
  • C 의 포인터 사용 증가로 인해 제한을 안전하게 사용하는 것이 더욱 어려워졌습니다.
  • 그러나 많은 컴파일러에는 __restrict__ 확장 기능이 있습니다.

결론

제한된 경우에 제한을 사용하면 성능이 향상될 수 있지만 몇 가지 중요한 함정이 있습니다. 제한 사용을 고려하고 있다면 먼저 코드를 프로파일링하세요.

현명하게 사용하세요.

릴리스 선언문 이 글은 https://dev.to/pauljlucas/the-obscure-restrict-keyword-in-c-2541에 복제되어 있습니다. 침해 내용이 있는 경우, [email protected]으로 연락하여 삭제하시기 바랍니다.
최신 튜토리얼 더>

부인 성명: 제공된 모든 리소스는 부분적으로 인터넷에서 가져온 것입니다. 귀하의 저작권이나 기타 권리 및 이익이 침해된 경우 자세한 이유를 설명하고 저작권 또는 권리 및 이익에 대한 증거를 제공한 후 이메일([email protected])로 보내주십시오. 최대한 빨리 처리해 드리겠습니다.

Copyright© 2022 湘ICP备2022001581号-3