Uso realista da palavra-chave 'restringir' C99?

183

Eu estava navegando através de alguma documentação e perguntas / respostas e vi isso mencionado. Eu li uma breve descrição, afirmando que seria basicamente uma promessa do programador de que o ponteiro não será usado para apontar para outro lugar.

Alguém pode oferecer alguns casos realistas em que vale a pena usá-lo?

user90052
fonte
4
memcpyvs memmoveé um exemplo canônico.
Alexandre C.
@AlexandreC .: Eu não acho que seja particularmente aplicável, uma vez que a falta de um qualificador "restrito" não implica que a lógica do programa funcione com sobrecarga de origem e destino, nem a presença de um qualificador impediria o uso de um método chamado. determinando se a origem e o destino se sobrepõem e, em caso afirmativo, substituindo dest por src + (dest-src) que, como é derivado de src, teria o apelido de alias.
Supercat
@ supercat: É por isso que eu coloco isso como um comentário. No entanto, 1) restrictargumentos qualificados para permitir memcpyque, em princípio, uma implementação ingênua seja otimizada de forma agressiva, e 2) a simples chamada memcpypermite que o compilador assuma que os argumentos dados a ele não possuem um alias, o que poderia permitir alguma otimização em torno da memcpychamada.
Alexandre C.
@AlexandreC .: Seria muito difícil para um compilador na maioria das plataformas otimizar um memcpy ingênuo - mesmo com "restringir" - ser quase tão eficiente quanto a versão adaptada ao destino. As otimizações do lado da chamada não exigiriam a palavra-chave "restringir" e, em alguns casos, os esforços para facilitar essas podem ser contraproducentes. Por exemplo, muitas implementações do memcpy podem, com custo extra zero, considerar memcpy(anything, anything, 0);um no-op e garantir que se pé um ponteiro para pelo menos nbytes graváveis memcpy(p,p,n); não terá efeitos colaterais adversos. Tais casos podem surgir ... #
286
... naturalmente em certos tipos de código de aplicativo (por exemplo, uma rotina de classificação trocando um item por si próprio) e em implementações em que não tenham efeitos colaterais adversos, permitir que esses casos sejam tratados pelo código de caso geral pode ser mais eficiente do que ter para adicionar testes de casos especiais. Infelizmente, alguns escritores de compiladores parecem achar que é melhor exigir que os programadores adicionem código que o compilador talvez não consiga otimizar, de modo a facilitar as "oportunidades de otimização" que os compiladores raramente explorariam de qualquer maneira.
supercat

Respostas:

182

restrictdiz que o ponteiro é a única coisa que acessa o objeto subjacente. Elimina o potencial de aliasing de ponteiro, permitindo uma melhor otimização pelo compilador.

Por exemplo, suponha que eu tenha uma máquina com instruções especializadas que possam multiplicar vetores de números na memória e que possua 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];
    }
}

O compilador precisa manipular adequadamente se dest, src1e se src2sobrepõe, o que significa que deve fazer uma multiplicação por vez, do início ao fim. Por isso restrict, o compilador pode otimizar esse código usando as instruções do vetor.

A Wikipedia tem uma entrada aqui restrict, com outro exemplo, aqui .

Michael
fonte
3
@ Michael - Se não me engano, o problema seria apenas quando destsobrepor um dos vetores de origem. Por que haveria um problema se src1e se src2sobrepõem?
ysap
1
O restringir normalmente só tem efeito ao apontar para um objeto que é modificado; nesse caso, afirma que nenhum efeito colateral oculto precisa ser levado em consideração. A maioria dos compiladores o utiliza para facilitar a vetorização. O Msvc usa a verificação do tempo de execução para sobreposição de dados para esse fim.
tim18 21/05
A adição da palavra-chave register à variável de loop for também a torna mais rápida, além de adicionar restringir.
2
Na verdade, a palavra-chave register é apenas um aviso. E nos compiladores desde o ano 2000, o i (e o n para comparação) no exemplo serão otimizados em um registro, independentemente de você usar ou não a palavra-chave register.
Mark Fischler
154

O exemplo da Wikipedia é muito esclarecedor.

Mostra claramente como 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?

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, 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 pelo 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;
    }
}

Por causa disso restrict, um compilador inteligente (ou humano) pode otimizar isso para:

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

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?

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 memsetchamadas 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.

C99

Vejamos o padrão por uma questão de integridade.

restrictdiz 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 em tempo de compilação.

Se o chamador não seguir o restrictcontrato, 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.

Regra estrita de alias

A restrictpalavra-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?

Veja também

Ciro Santilli adicionou uma nova foto
fonte
9
O qualificador "restringir" pode realmente permitir economias muito maiores. Por exemplo, dados void zap(char *restrict p1, char *restrict p2) { for (int i=0; i<50; i++) { p1[i] = 4; p2[i] = 9; } }, os qualificadores restritos permitiriam que o compilador reescrevesse o código como "memset (p1,4,50); memset (p2,9,50);". Restringir é muito superior ao aliasing baseado em tipo; é uma pena que os compiladores se concentrem mais nesse último.
supercat
@supercat ótimo exemplo, adicionado à resposta.
Ciro Santilli # 9/16
2
@ tim18: a palavra-chave "restringir" pode permitir muitas otimizações que mesmo a otimização agressiva baseada em tipo não pode. Além disso, a existência de "restringir" na linguagem - diferentemente do aliasing agressivo baseado em tipos - nunca impossibilita a execução de tarefas com a maior eficiência possível na sua ausência (uma vez que o código que seria quebrado por "restringir" pode simplesmente não usá-lo, enquanto o código que é quebrado pelo agressivo TBAA geralmente deve ser reescrito de maneira menos eficiente).
Supercat 23/05
2
@ tim18: coloque itens que contenham sublinhado duplo nos backticks, como em __restrict. Caso contrário, os sublinhados duplos podem ser mal interpretados como uma indicação de que você está gritando.
Supercat
1
Mais importante do que não gritar é que os sublinhados têm um significado diretamente relevante para o ponto que você está tentando enfatizar.
remcycles