El Wikipedia example es muy illuminating.
Muestra claramente cómo permite guardar una instrucción de montaje.
Sin restringir:
void f(int *a, int *b, int *x) {
*a += *x;
*b += *x;
}
Pseudo montaje:
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
Con restringir:
void fr(int *restrict a, int *restrict b, int *restrict x);
Pseudo montaje:
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
¿GCC realmente lo hace?
GCC 4.8 Linux x86-64:
gcc -g -std=c99 -O0 -c main.c
objdump -S main.o
Con -O0
, que son los mismos.
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 calling convention 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 frente a 3 instrucciones.
matrices
Hasta ahora hemos ahorro de instrucciones individuales, pero si el puntero representan matrices para ser puesto en loop, un caso de uso común, entonces un montón de instrucciones podrían salvarse, como se ha mencionado por supercat.
Considérese, por ejemplo:
void f(char *restrict p1, char *restrict p2) {
for (int i = 0; i < 50; i++) {
p1[i] = 4;
p2[i] = 9;
}
}
Debido a restrict
, un compilador inteligente (o humano), podría optimizar que a:
memset(p1, 4, 50);
memset(p2, 9, 50);
que es potencialmente mucho más eficiente, ya que puede ser de montaje optimizado en una implementación de libc decente (como glibc): Is it better to use std::memcpy() or std::copy() in terms to performance?
¿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 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
Dos memset
llamadas como se esperaba.
sin restringir: no hay llamadas stdlib, sólo un 16 iteración amplia loop unrolling, que no tengo la intención de reproducir aquí :-)
no he tenido la paciencia de referencia, pero creo que la versión restringida será más rápida.
C99
Veamos el estándar por el bien integridad.
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 invocar la función, pero permite más optimizaciones en tiempo de compilación.
Si la persona que llama no sigue el contrato restrict
, comportamiento indefinido.
Los C99 N1256 draft 6.7.3/7 "calificadores de tipo" dice:
El uso previsto de la clasificación para restringir (como la clase de registros de almacenamiento) es promover la optimización y eliminación de todas las instancias del calificador de todas las unidades de traducción de preprocesamiento que componen un programa conforme no cambian su significado (es decir, el comportamiento observable).
y 6.7.3.1 "Definición formal de restringir" da los detalles sangrientos.
regla de alias estricto
La palabra clave restrict
sólo afecta a los punteros de tipos compatibles (por ejemplo, dos int*
) debido a las estrictas reglas de alias dice que aliasing tipos incompatibles es un comportamiento no definido por defecto, y así compiladores puede asumir que lo hace no sucede y optimiza de distancia.
Ver: What is the strict aliasing rule?
Véase también
'memcpy' vs' memmove' es un ejemplo canónico. –
@AlexandreC .: No creo que sea particularmente aplicable, ya que la falta de un calificador "restringido" no implica que la lógica del programa funcione con una fuente y un destino de sobrecarga, ni la presencia de dicho calificador impedirá un método llamado de determinar si la fuente y el destino se superponen y, de ser así, reemplazar dest con src + (dest-src) que, dado que se deriva de src, se le permitiría alias. – supercat
@supercat: Es por eso que lo puse como comentario. Sin embargo, 1) restringir los argumentos de calificación a 'memcpy' permite en principio una implementación ingenua para ser optimizada de forma agresiva, y 2) simplemente llamar a' memcpy' permite al compilador asumir que los argumentos que se le dan no alias, lo que podría permitir algo de optimización alrededor de la llamada 'memcpy'. –