Como otros dijeron, si no significa nada a partir de C ++ 14 , entonces consideremos la __restrict__
extensión GCC que hace lo mismo que el C99restrict
.
C99
restrict
dice que dos punteros no pueden apuntar a regiones de memoria superpuestas. El uso más común es para argumentos de función.
Esto restringe cómo se puede llamar a la función, pero permite más optimizaciones de compilación.
Si la persona que llama no sigue el restrict
contrato, comportamiento indefinido.
El C99 N1256 borrador 6.7.3 / 7 "Calificadores de tipo" dice:
El uso previsto del calificador de restricción (como la clase de almacenamiento de registro) es para promover la optimización, y eliminar todas las instancias del calificador de todas las unidades de traducción de preprocesamiento que componen un programa conforme no cambia su significado (es decir, comportamiento observable).
y 6.7.3.1 "Definición formal de restricción" da los detalles sangrientos.
Una posible optimización
El ejemplo de Wikipedia es muy esclarecedor.
Muestra claramente cómo, ya que permite guardar una instrucción de ensamblaje .
Sin restricción:
void f(int *a, int *b, int *x) {
*a += *x;
*b += *x;
}
Pseudo ensamblaje:
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
Con restringir:
void fr(int *restrict a, int *restrict b, int *restrict x);
Pseudo ensamblaje:
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 realmente lo hace?
g++
4.8 Linux x86-64:
g++ -g -std=gnu++98 -O0 -c main.cpp
objdump -S main.o
Con -O0
, son lo mismo.
Con -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 los no iniciados, la convención de convocatoria es:
rdi
= primer parámetro
rsi
= segundo parámetro
rdx
= tercer parámetro
La salida de GCC fue incluso más clara que el artículo wiki: 4 instrucciones vs 3 instrucciones.
Matrices
Hasta ahora, tenemos ahorros en una sola instrucción, pero si el puntero representa las matrices que se van a recorrer, un caso de uso común, entonces se podrían guardar un montón de instrucciones, como lo mencionaron supercat y michael .
Considere por ejemplo:
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;
}
}
Debido a que restrict
un compilador inteligente (o humano), podría optimizar eso para:
memset(p1, 4, size);
memset(p2, 9, size);
¿Qué es potencialmente mucho más eficiente, ya que puede ser ensamblado optimizado en una implementación decente de libc (como glibc) ¿Es mejor usar std :: memcpy () o std :: copy () en términos de rendimiento? , posiblemente con instrucciones SIMD .
Sin, restringir, esta optimización no podría hacerse, por ejemplo, considere:
char p1[4];
char *p2 = &p1[1];
f(p1, p2, 3);
Entonces la for
versión hace:
p1 == {4, 4, 4, 9}
mientras que la memset
versión hace:
p1 == {4, 9, 9, 9}
¿GCC realmente lo hace?
GCC 5.2.1.Linux x86-64 Ubuntu 15.10:
gcc -g -std=c99 -O0 -c main.c
objdump -dr main.o
Con -O0
ambos son lo mismo.
Con -O3
:
con restricción:
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
Dos memset
llamadas como se esperaba.
sin restricción: no hay llamadas stdlib, solo un desenrollamiento de bucle ancho de 16 iteraciones que no pretendo reproducir aquí :-)
No he tenido la paciencia para compararlos, pero creo que la versión restringida será más rápida.
Regla de alias estricta
La restrict
palabra clave solo afecta a punteros de tipos compatibles (por ejemplo, dos int*
) porque las estrictas reglas de alias dicen que el alias de tipos incompatibles es un comportamiento indefinido por defecto, por lo que los compiladores pueden asumir que no sucede y optimizar.
Ver: ¿Cuál es la estricta regla de alias?
¿Funciona para referencias?
De acuerdo con los documentos de GCC, sí lo hace: https://gcc.gnu.org/onlinedocs/gcc-5.1.0/gcc/Restricted-Pointers.html con sintaxis:
int &__restrict__ rref
Incluso hay una versión para las this
funciones miembro:
void T::fn () __restrict__
restrict
es una palabra clave c99. Sí, Rpbert S. Barnes, sé que la mayoría de los compiladores son compatibles__restrict__
. Notará que cualquier cosa con guiones bajos dobles es, por definición, específica de implementación y, por lo tanto, NO C ++ , sino una versión específica del compilador.