uso realista do C99 'restringir' palavra-chave?
-
09-09-2019 - |
Pergunta
eu estava navegando através de alguma documentação e perguntas / respostas e vi que mencionou. Eu li uma descrição breve, afirmando que seria basicamente uma promessa de que o programador que o ponteiro não será usado para ponto em outro lugar.
Alguém pode oferecer alguns casos realistas, onde o que vale realmente usando isso?
Solução
restrict
diz que o ponteiro é a única coisa que acessa o objeto subjacente. Ele elimina a possibilidade de aliasing ponteiro, permitindo uma melhor optimização pelo compilador.
Por exemplo, suponha que eu tenho uma máquina com instruções especializadas que podem se multiplicar vetores de números na memória, e eu tenho o seguinte código:
void MultiplyArrays(int* dest, int* src1, int* src2, int n)
{
for(int i = 0; i < n; i++)
{
dest[i] = src1[i]*src2[i];
}
}
As necessidades do compilador para tratar adequadamente se dest
, src1
e sobreposição src2
, o que significa que deve fazer uma multiplicação de cada vez, do início ao fim. Por ter restrict
, o compilador é livre para otimizar este código usando as instruções do vetor.
A Wikipedia tem uma entrada sobre restrict
, com outro exemplo, aqui .
Outras dicas
O Wikipedia exemplo é muito esclarecedora.
Ele claramente mostra como permite salvar uma instrução de montagem .
Sem restringir:
void f(int *a, int *b, int *x) {
*a += *x;
*b += *x;
}
Pseudo assembly:
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 a may be equal to x.
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 assembly:
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
load R2 ← *b
add R2 += R1
set R2 → *b
O GCC realmente fazê-lo?
GCC 4.8 Linux x86-64:
gcc -g -std=c99 -O0 -c main.c
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, o convenção de chamada é:
-
rdi
= primeiro parâmetro -
rsi
= segundo parâmetro -
rdx
= terceiro parâmetro
saída GCC foi ainda mais claro que o artigo wiki:. 4 instruções vs 3 instruções
Arrays
Até agora, temos poupança de instrução individuais, mas se ponteiro representam matrizes para ser enrolada ao longo, um caso de uso comum, então um monte de instruções poderiam ser salvas, como mencionado por supercat .
Considere, por exemplo:
void f(char *restrict p1, char *restrict p2) {
for (int i = 0; i < 50; i++) {
p1[i] = 4;
p2[i] = 9;
}
}
Devido restrict
, um compilador inteligente (ou humano), pode optimizar a que:
memset(p1, 4, 50);
memset(p2, 9, 50);
que é potencialmente muito mais eficiente, uma vez que pode ser montagem otimizada em uma implementação decente libc (como glibc): é melhor usar std :: memcpy () ou std :: copy () em termos de desempenho?
O GCC realmente fazê-lo?
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 o mesmo.
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
chamadas
memset
Dois conforme esperado. -
sem restringir: nenhuma chamada stdlib, apenas a 16 iteração ampla circuito desenrolando que não tenho a intenção de reproduzir aqui: -)
Eu não tive a paciência de referência-los, mas eu acredito que a versão restringir será mais rápido.
C99
Vamos olhar o padrão para sermos mais completos.
restrict
diz que dois ponteiros não pode apontar para sobreposição de regiões de memória. O uso mais comum é para os argumentos da função.
Isto restringe como a função pode ser chamado, mas permite mais otimizações de tempo de compilação.
Se o chamador não siga o contrato restrict
, comportamento indefinido.
O C99 N1256 projecto 6.7. 3/7 "Tipo qualificadores" diz:
O uso pretendido do restringir o qualificador (como a classe de armazenamento registo) é promover a otimização e excluir todas as instâncias do qualificador de todas as unidades de tradução de pré-processamento que compõem um programa de conformidade não muda o seu significado (ie, comportamento observável).
e 6.7.3.1 "definição formal de restringir" dá os detalhes.
regra aliasing estrita ??strong>
A palavra-chave restrict
afeta somente ponteiros de tipos compatíveis (por exemplo, dois int*
) porque as regras aliasing estritas diz que aliasing tipos incompatíveis é um comportamento indefinido por padrão, e assim por compiladores pode supor que isso não aconteça e otimizar distância.
Veja: Qual é a regra aliasing estrita
?Veja também
- C ++ 14 ainda não tem um análogo para
restrict
, mas GCC tem__restrict__
como uma extensão: o que a palavra-chave média em C ++? - Muitas perguntas que pedem: de acordo com os detalhes, faz este código UB ou não?
- A "quando usar" pergunta: Quando a utilização restringir e quando não
- O
__attribute__((malloc))
GCC relacionada, que diz que o valor de retorno de uma função não é alias a qualquer coisa: GCC: __attribute__ ((malloc))