Como outros disseram, se não significa nada como no C ++ 14 , vamos considerar a __restrict__
extensão GCC, que faz o mesmo que o C99 restrict
.
C99
restrict
diz que dois ponteiros não podem apontar para regiões de memória sobrepostas. O uso mais comum é para argumentos de função.
Isso restringe como a função pode ser chamada, mas permite mais otimizações de compilação.
Se o chamador não seguir o restrict
contrato, comportamento indefinido.
O projeto C99 N1256 6.7.3 / 7 "Qualificadores de tipo" diz:
O uso pretendido do qualificador restrito (como a classe de armazenamento de registro) é promover a otimização e excluir todas as instâncias do qualificador de todas as unidades de conversão de pré-processamento que compõem um programa em conformidade não altera seu significado (ou seja, comportamento observável).
e 6.7.3.1 "Definição formal de restrição" fornece detalhes sangrentos.
Uma possível otimização
O exemplo da Wikipedia é muito esclarecedor.
Mostra claramente como ele permite salvar uma instrução de montagem .
Sem restrição:
void f(int *a, int *b, int *x) {
*a += *x;
*b += *x;
}
Pseudo montagem:
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
Com restringir:
void fr(int *restrict a, int *restrict b, int *restrict x);
Pseudo montagem:
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
O GCC realmente faz isso?
g++
4.8 Linux x86-64:
g++ -g -std=gnu++98 -O0 -c main.cpp
objdump -S main.o
Com -O0
, eles são os mesmos.
Com -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)
Para os não iniciados, a convenção de chamada é:
rdi
= primeiro parâmetro
rsi
= segundo parâmetro
rdx
= terceiro parâmetro
A saída do GCC foi ainda mais clara que o artigo da wiki: 4 instruções vs 3 instruções.
Matrizes
Até o momento, temos economia de instruções únicas, mas se o ponteiro representar matrizes a serem repetidas, um caso de uso comum, várias instruções podem ser salvas, como mencionado por supercat e michael .
Considere, por exemplo:
void f(char *restrict p1, char *restrict p2, size_t size) {
for (size_t i = 0; i < size; i++) {
p1[i] = 4;
p2[i] = 9;
}
}
Por causa disso restrict
, um compilador inteligente (ou humano) pode otimizar isso para:
memset(p1, 4, size);
memset(p2, 9, size);
O que é potencialmente muito mais eficiente, pois pode ser otimizado para montagem em uma implementação decente da libc (como glibc). É melhor usar std :: memcpy () ou std :: copy () em termos de desempenho? , possivelmente com instruções SIMD .
Sem, restringir, essa otimização não pode ser feita, por exemplo, considere:
char p1[4];
char *p2 = &p1[1];
f(p1, p2, 3);
Então a for
versão faz:
p1 == {4, 4, 4, 9}
enquanto a memset
versão faz:
p1 == {4, 9, 9, 9}
O GCC realmente faz isso?
GCC 5.2.1.Linux x86-64 Ubuntu 15.10:
gcc -g -std=c99 -O0 -c main.c
objdump -dr main.o
Com -O0
, ambos são iguais.
Com -O3
:
com restringir:
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
Duas memset
chamadas conforme o esperado.
sem restrição: sem chamadas stdlib, apenas um loop de 16 iterações que eu não pretendo reproduzir aqui :-)
Não tive paciência para compará-los, mas acredito que a versão restrita será mais rápida.
Regra estrita de alias
A restrict
palavra-chave afeta apenas ponteiros de tipos compatíveis (por exemplo, dois int*
) porque as regras estritas de aliasing dizem que o aliasing de tipos incompatíveis é um comportamento indefinido por padrão, e assim os compiladores podem assumir que isso não acontece e otimizar.
Veja: Qual é a regra estrita de alias?
Isso funciona para referências?
De acordo com os documentos do GCC, ele faz: https://gcc.gnu.org/onlinedocs/gcc-5.1.0/gcc/Restricted-Pointers.html com sintaxe:
int &__restrict__ rref
Existe até uma versão para this
funções-membro:
void T::fn () __restrict__
restrict
é uma palavra-chave c99. Sim, Rpbert S. Barnes, eu sei que a maioria dos compiladores suporta__restrict__
. Você notará que qualquer coisa com sublinhado duplo é, por definição, específica da implementação e, portanto, NÃO C ++ , mas uma versão específica do compilador.