C99 '제한'키워드의 현실적인 사용법?


183

나는 몇 가지 문서와 질문 / 답변을 탐색하면서 언급 된 것을 보았습니다. 기본적으로 프로그래머가 포인터가 다른 곳을 가리키는 데 사용되지 않는다는 약속이라고 간략히 설명했습니다.

누구나 실제로 이것을 사용할 가치가있는 현실적인 사례를 제공 할 수 있습니까?


4
memcpyvs memmove는 하나의 표준 예입니다.
Alexandre C.

@AlexandreC .: "제한적"한정자가 부족하더라도 프로그램 논리가 소스 및 대상을 오버로드 할 때 작동한다는 것을 의미하지는 않으며, 그러한 한정자가 있으면 호출 된 메소드를 막을 수 없습니다. 소스와 대상이 겹치는 지 여부를 판별하고, 그렇다면 src에서 파생 된 dest를 src + (dest-src)로 바꾸어 별명을 지정할 수 있습니다.
supercat

@ supercat : 그것이 의견으로 쓰인 이유입니다. 그러나 1) restrict인수를 한정 memcpy하면 원칙적으로 순진한 구현을 적극적으로 최적화 할 수 있으며 2) 단순히 호출 memcpy하면 컴파일러가 주어진 인수가 별칭이 아니라고 가정하여 memcpy호출을 최적화 할 수 있습니다 .
Alexandre C.

@AlexandreC .: 대부분의 플랫폼에서 컴파일러가 "restrict"를 사용하여 순진한 memcpy를 대상에 맞게 조정 된 버전만큼 효율적으로 만드는 것은 매우 어려울 것입니다. 콜 사이드 최적화에는 "제한"키워드가 필요하지 않으며, 경우에 따라이를 촉진하려는 노력이 역효과를 낳을 수도 있습니다. 예를 들어, memcpy의 많은 구현은 추가 비용없이 제로 ( memcpy(anything, anything, 0);no-op)로 간주 할 수 있으며 if p가 최소한 n쓰기 가능한 바이트에 대한 포인터 인지 확인합니다 memcpy(p,p,n). 부작용은 없습니다. 이러한 경우가 발생할 수 있습니다.
supercat

... 자연스럽게 특정 종류의 응용 프로그램 코드 (예를 들어, 항목을 자체와 교환하는 정렬 루틴)에서 부작용이없는 구현에서는 이러한 경우를 일반적인 경우 코드로 처리하는 것이 더 효율적일 수 있습니다 특별한 경우 테스트를 추가합니다. 불행히도 일부 컴파일러 작성자는 프로그래머가 컴파일러가 최적화 할 수없는 코드를 추가하여 컴파일러가 어쨌든 거의 활용하지 않는 "최적화 기회"를 촉진하도록 요구하는 것이 더 낫다고 생각합니다.
supercat

답변:


182

restrict포인터는 기본 객체에 액세스하는 유일한 것입니다. 포인터 앨리어싱의 가능성을 제거하여 컴파일러의 최적화를 향상시킵니다.

예를 들어 메모리에 숫자 벡터를 곱할 수있는 특수 명령이있는 머신이 있고 다음 코드가 있다고 가정합니다.

void MultiplyArrays(int* dest, int* src1, int* src2, int n)
{
    for(int i = 0; i < n; i++)
    {
        dest[i] = src1[i]*src2[i];
    }
}

컴파일러는 if dest, src1src2겹침 을 올바르게 처리 해야합니다. 즉, 처음부터 끝까지 한 번에 하나씩 곱해야합니다. 를 가짐 restrict으로써 컴파일러는 벡터 명령어를 사용하여이 코드를 자유롭게 최적화 할 수 있습니다.

Wikipedia에는에 대한 항목이 있으며 여기restrict 에는 다른 예가 있습니다 .


3
@Michael-내가 착각하지 않으면 dest소스 벡터 중 하나와 겹치는 경우 에만 문제가 발생 합니다. 왜 문제가있는 경우가있을 것입니다 src1src2중복?
ysap

1
제한은 일반적으로 수정 된 객체를 가리킬 때만 영향을 미치며,이 경우 숨겨진 부작용을 고려할 필요가 없습니다. 대부분의 컴파일러는이를 사용하여 벡터화를 용이하게합니다. Msvc는 해당 목적을 위해 데이터 중복에 대해 런타임 검사를 사용합니다.
tim18

for 루프 변수에 register 키워드를 추가하면 제한을 추가하는 것 외에도 더 빠릅니다.

2
실제로 register 키워드는 단지 자문 일뿐입니다. 그리고 2000 년경부터 컴파일러에서 예제의 i (및 비교를위한 n)는 레지스터 키워드 사용 여부에 관계없이 레지스터로 최적화됩니다.
Mark Fischler

154

위키 백과의 예 입니다 매우 조명.

하나의 조립 명령을 저장 하는 방법을 명확하게 보여줍니다. .

제한없이 :

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

의사 어셈블리 :

load R1  *x    ; Load the value of x pointer
load R2  *a    ; Load the value of a pointer
add R2 += R1    ; Perform Addition
set R2  *a     ; Update the value of a pointer
; Similarly for b, note that x is loaded twice,
; because x may point to a (a aliased by x) thus 
; the value of x will change when the value of a
; changes.
load R1  *x
load R2  *b
add R2 += R1
set R2  *b

제한으로 :

void fr(int *restrict a, int *restrict b, int *restrict x);

의사 어셈블리 :

load R1  *x
load R2  *a
add R2 += R1
set R2  *a
; Note that x is not reloaded,
; because the compiler knows it is unchanged
; "load R1 ← *x" is no longer needed.
load R2  *b
add R2 += R1
set R2  *b

GCC가 실제로합니까?

GCC 4.8 Linux x86-64 :

gcc -g -std=c99 -O0 -c main.c
objdump -S main.o

-O0 동일합니다.

-O3:

void f(int *a, int *b, int *x) {
    *a += *x;
   0:   8b 02                   mov    (%rdx),%eax
   2:   01 07                   add    %eax,(%rdi)
    *b += *x;
   4:   8b 02                   mov    (%rdx),%eax
   6:   01 06                   add    %eax,(%rsi)  

void fr(int *restrict a, int *restrict b, int *restrict x) {
    *a += *x;
  10:   8b 02                   mov    (%rdx),%eax
  12:   01 07                   add    %eax,(%rdi)
    *b += *x;
  14:   01 06                   add    %eax,(%rsi) 

시작하지 않은 경우 호출 규칙 은 다음과 같습니다.

  • rdi = 첫 번째 매개 변수
  • rsi = 두 번째 매개 변수
  • rdx = 세번째 매개 변수

GCC 출력은 Wiki 기사보다 훨씬 명확했습니다. 4 가지 명령어와 3 개의 명령어.

배열

지금까지 단일 명령 절감 효과가 있지만 포인터가 반복되는 배열, 일반적인 사용 사례를 나타내는 경우 supercat에서 언급 한 것처럼 많은 명령을 저장할 수 있습니다 .

예를 들면 다음과 같습니다.

void f(char *restrict p1, char *restrict p2) {
    for (int i = 0; i < 50; i++) {
        p1[i] = 4;
        p2[i] = 9;
    }
}

때문에 restrict스마트 컴파일러 (또는 사람)는 다음과 같이 최적화 할 수 있습니다.

memset(p1, 4, 50);
memset(p2, 9, 50);

glibc와 같은 괜찮은 libc 구현에서 어셈블리 최적화 될 수 있기 때문에 잠재적으로 훨씬 효율적입니다. 성능 측면에서 std :: memcpy () 또는 std :: copy ()를 사용하는 것이 더 낫습니까?

GCC가 실제로합니까?

GCC 5.2.1. 리눅스 x86-64 우분투 15.10 :

gcc -g -std=c99 -O0 -c main.c
objdump -dr main.o

-O0 하면 둘 다 동일합니다.

-O3:

  • 제한으로 :

    3f0:   48 85 d2                test   %rdx,%rdx
    3f3:   74 33                   je     428 <fr+0x38>
    3f5:   55                      push   %rbp
    3f6:   53                      push   %rbx
    3f7:   48 89 f5                mov    %rsi,%rbp
    3fa:   be 04 00 00 00          mov    $0x4,%esi
    3ff:   48 89 d3                mov    %rdx,%rbx
    402:   48 83 ec 08             sub    $0x8,%rsp
    406:   e8 00 00 00 00          callq  40b <fr+0x1b>
                            407: R_X86_64_PC32      memset-0x4
    40b:   48 83 c4 08             add    $0x8,%rsp
    40f:   48 89 da                mov    %rbx,%rdx
    412:   48 89 ef                mov    %rbp,%rdi
    415:   5b                      pop    %rbx
    416:   5d                      pop    %rbp
    417:   be 09 00 00 00          mov    $0x9,%esi
    41c:   e9 00 00 00 00          jmpq   421 <fr+0x31>
                            41d: R_X86_64_PC32      memset-0x4
    421:   0f 1f 80 00 00 00 00    nopl   0x0(%rax)
    428:   f3 c3                   repz retq

    memset예상대로 두 번의 통화.

  • 제한없이 : stdlib 호출 없음, 여기에서 재현하지 않으려 는 16 개의 반복 너비 루프 언 롤링 :-)

나는 그것들을 벤치마킹 할 인내가 없었지만 제한 버전이 더 빠를 것이라고 믿습니다.

C99

완전성을위한 표준을 살펴 보자.

restrict두 포인터가 겹치는 메모리 영역을 가리킬 수 없다고 말합니다. 가장 일반적인 사용법은 함수 인수입니다.

이것은 함수 호출 방법을 제한하지만 더 많은 컴파일 타임 최적화를 허용합니다.

발신자가 restrict계약을 따르지 않으면 정의되지 않은 동작입니다.

C99 N1256 초안 6.7.3 / 7 "형식 한정자"말한다 :

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

그리고 6.7.3.1 "제한의 공식적 정의"는 세부 사항을 제공한다.

엄격한 앨리어싱 규칙

restrict키워드는 호환 가능한 유형의 포인터 (예 : 두에 영향을 미치는 int*엄격한 앨리어싱 규칙이 호환되지 않는 유형의 별명을하는 것은 기본적으로 정의되지 않은 동작이라고 말한다 때문에), 그리고 컴파일러는 가정 할 수 있도록 멀리 일어날 최적화하지 않습니다.

참조 : 엄격한 앨리어싱 규칙은 무엇입니까?

또한보십시오


9
"제한"한정자는 실제로 훨씬 더 많은 비용을 절약 할 수 있습니다. 예를 들어, void zap(char *restrict p1, char *restrict p2) { for (int i=0; i<50; i++) { p1[i] = 4; p2[i] = 9; } }한정 한정자는 컴파일러가 코드를 "memset (p1,4,50); memset (p2,9,50);"으로 다시 쓰도록합니다. 제한은 유형 기반 앨리어싱보다 훨씬 우수합니다. 그것은 후자에 더 초점을 맞춘 수치스러운 컴파일러입니다.
supercat

@ supercat 훌륭한 예가 대답에 추가되었습니다.
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功

2
@ tim18 : "restrict"키워드는 공격적인 유형 기반 최적화조차도 할 수없는 많은 최적화를 가능하게합니다. 또한 공격적인 유형 기반의 앨리어싱과 달리 언어에 "제한"이 존재하면 부재시 가능한 한 효율적으로 작업을 수행 할 수 없습니다. 사용하지 마십시오. 공격적인 TBAA에 의해 손상된 코드는 종종 비효율적 인 방식으로 다시 작성되어야합니다).
supercat

2
@ tim18 :와 같이 백틱으로 이중 밑줄이 포함 된 것들을 둘러싸십시오 __restrict. 그렇지 않으면 이중 밑줄이 소리 쳤다는 표시로 잘못 해석 될 수 있습니다.
supercat

1
외치지 않는 것보다 더 중요한 것은 밑줄은 사용자가 만들려는 요점과 직접적으로 관련이 있다는 의미입니다.
remcycles
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.