Entre outras coisas, C99 adicionou a palavra-chavestrict como uma forma de um programador especificar que um ponteiro é o único ponteiro para um determinado objeto em um escopo e, conseqüentemente, dar ao compilador uma “dica ”que pode realizar otimizações adicionais ao acessar o objeto por meio desse ponteiro.
Para ilustrar o problema que a restrição deveria resolver, considere uma função como:
void update_ptrs( int *p, int *q, int const *v ) { *p = *v; *q = *v; }
para o qual o compilador irá gerar código x86-64 como:
mov eax, [rdx] ; tmp = *v // 1 add [rdi], eax ; *p = tmp mov eax, [rdx] ; tmp = *v // 3 add [rsi], eax ; *q = tmp
Você pode estar se perguntando por que ele gera a linha 3, já que parece redundante com a linha 1. O problema é que o compilador não pode saber que você não fez algo assim:
int x = 1, v = 2; update_ptrs( &v, &x, &v ); // x = 5, v = 4
Em update_ptrs(), p e v seriam alias o mesmo int, então o compilador tem que jogar pelo seguro e assumir que o valor de *v pode mudar entre leituras, daí a instrução mov adicional.
Em geral, ponteiros em C confundem a otimização, pois o compilador não consegue saber se dois ponteiros se alias. Em código de desempenho crítico, eliminar leituras de memória poderia ser uma grande vitória se o compilador pudesse fazer isso com segurança.
Para resolver o problema mencionado acima, restrito foi adicionado a C para permitir que você especifique que um determinado ponteiro é o único ponteiro para um objeto no escopo do ponteiro, ou seja, nenhum outro ponteiro no mesmo aliases de escopo isto.
Para usar restringir, você insere-o entre o * e o nome do ponteiro em uma declaração. Um update_ptrs() reescrito para usar restrição seria:
void update_ptrs_v2( int *restrict p, int *restrict q, int const *restrict v ) { *p = *v; *q = *v; }
(Leia da direita para a esquerda, por exemplo, v é um ponteiro restrito para uma constante int; ou use cdecl.)
Ao adicionar restrição, o compilador agora pode gerar código como:
mov eax, [rdx] ; tmp = *v add [rdi], eax ; *p = tmp add [rsi], eax ; *q = tmp
Agora, o compilador foi capaz de elidir a linha 3 anterior da instrução mov adicional.
Talvez o exemplo mais conhecido onde a restrição é usada seja a função de biblioteca padrão memcpy(). É a maneira mais rápida de copiar um pedaço de memória se os endereços de origem e destino não se sobrepõem. A função memmove() um pouco mais lenta existe para uso quando os endereços do se sobrepõem.
O uso indevido de restrição resulta em comportamento indefinido, por exemplo, passando ponteiros que fazem alias entre si para update_ptrs_v2() ou memcpy(). Em alguns casos, o compilador pode avisá-lo, mas não em todos os casos, portanto, não confie no compilador para detectar usos indevidos.
Observe que a restrição é para um determinado escopo. Atribuir um ponteiro restrito a outro no mesmo escopo resulta em comportamento indefinido:
void f( int *restrict d, int *restrict s ) { int *restrict p = s; // undefined behavior
No entanto, você pode atribuir um ponteiro restrito a um ponteiro irrestrito:
void f( int *restrict d, int *restrict s ) { int *p = s; // OK
Mesmo que p seja irrestrito, o compilador ainda pode realizar as mesmas otimizações.
Também não há problema em atribuir um ponteiro restrito em um escopo interno a outro em um escopo externo (mas não o contrário):
void f( int *restrict d, int *restrict s ) { { // inner scope int *restrict p = s; // OK // ... s = p; // undefined behavior } }
Primeiro, você definitivamente deve criar um perfil de seu código (e talvez até mesmo olhar para o código assembly gerado) para ver se o uso de restrição realmente traz uma melhoria de desempenho significativa para justificar o risco de possíveis armadilhas. Diagnosticar bugs causados pelo uso indevido de restrição é muito difícil de fazer.
Segundo, se o uso de restrição estiver confinado à implementação de uma função onde a memória acessada por meio de ponteiros restritos foi alocada por você, então é mais seguro. Por exemplo, dado:
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 ); }
o código pode operar na primeira e segunda metades do array com segurança porque elas não se sobrepõem (assumindo que você nunca acesse half_1st[n/2] ou além).
Terceiro, se restringir for usado nos parâmetros de uma função, então é potencialmente menos seguro. Por exemplo, compare safer() com update_ptrs_v2() onde o caller controla os ponteiros. Pelo que você sabe, o chamador entendeu errado e passou ponteiros para esse alias.
Somente ponteiros para objetos (ou void) podem ser qualificados com restrição:
restrict int x; // error: can't restrict object int restrict *p; // error: pointer to restrict object int (*restrict f)(); // error: pointer-to-function
Você pode usar restrição para membros de struct, por exemplo:
struct node { void *restrict data; struct node *restrict left; struct node *restrict right; };
diz que os dados serão o único ponteiro para esses dados e que esquerda e direita nunca apontarão para o mesmo nó. No entanto, o uso de restrição para membros struct é muito incomum.
Por último, C não tem restrição. Por que não? A resposta é longa, mas a versão TL;DR é esta:
No entanto, muitos compiladores têm __restrict__ como extensão.
Em casos limitados, o uso de restrição pode levar a melhorias de desempenho, mas existem várias armadilhas significativas. Se você está pensando em usar restrição, crie primeiro o perfil do seu código.
Use com sabedoria.
Isenção de responsabilidade: Todos os recursos fornecidos são parcialmente provenientes da Internet. Se houver qualquer violação de seus direitos autorais ou outros direitos e interesses, explique os motivos detalhados e forneça prova de direitos autorais ou direitos e interesses e envie-a para o e-mail: [email protected]. Nós cuidaremos disso para você o mais rápido possível.
Copyright© 2022 湘ICP备2022001581号-3