modificada - ¿Qué significa la palabra clave restrict en C++?
palabras claves (6)
Siempre estuve inseguro, ¿qué significa la palabra clave restrictiva en C ++?
¿Significa que los dos o más punteros asignados a la función no se superponen? ¿Qué más significa?
Como los archivos de cabecera de algunas bibliotecas de C usan la palabra clave, el lenguaje de C ++ tendrá que hacer algo al respecto ... como mínimo, ignorando la palabra clave, por lo que no es necesario # definir la palabra clave en una macro en blanco para suprimir la palabra clave .
Como otros dijeron, si no significa nada a partir de C ++ 14 , consideremos la extensión __restrict__
GCC que hace lo mismo que la C99 restrict
.
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 invocar la función, pero permite más optimizaciones de compilación.
Si la persona que llama no sigue el contrato de restrict
, comportamiento indefinido.
El C99 N1256 borrador 6.7.3 / 7 "Calificadores de tipo" dice:
El uso previsto del calificador restrictivo (como la clase de almacenamiento de registro) es 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, el comportamiento observable).
y 6.7.3.1 "Definición formal de restringir" da los detalles sangrientos.
Una posible optimización
El ejemplo de Wikipedia es muy esclarecedor.
Muestra claramente cómo permite guardar una instrucción de ensamblaje .
Sin restricciones:
void f(int *a, int *b, int *x) {
*a += *x;
*b += *x;
}
Pseudo ensamblado:
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 ensamblado:
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?
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 llamadas 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 de wiki: 4 instrucciones vs 3 instrucciones.
Arrays
Hasta ahora, hemos ahorrado una sola instrucción, pero si el puntero representa las matrices que se deben bifurcar, un caso de uso común, entonces se podrían guardar un montón de instrucciones, como lo mencionan supercat y michael .
Considera 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 la restrict
, un compilador inteligente (o humano) podría optimizar eso para:
memset(p1, 4, size);
memset(p2, 9, size);
Que es potencialmente mucho más eficiente ya que puede ser optimizado para ensamblar en una implementación de libc decente (como glibc) ¿Es mejor usar std :: memcpy () o std :: copy () en términos de rendimiento? , posiblemente con instrucciones SIMD .
Sin, restringe, esta optimización no podría hacerse, por ejemplo, considere:
char p1[4];
char *p2 = &p1[1];
f(p1, p2, 3);
Luego, for
versión hace:
p1 == {4, 4, 4, 9}
mientras que la versión memset
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 iguales.
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 llamadas
memset
como se esperaba.sin restricciones: no llamadas stdlib, solo un despliegue de bucle de iteración 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.
Estricta regla de aliasing
La palabra clave restrict
solo afecta punteros de tipos compatibles (por ejemplo, dos int*
) porque las reglas de alias estrictas dicen que aliasing incompatible types es un comportamiento indefinido por defecto, por lo que los compiladores pueden asumir que no ocurre y optimizarlo.
Ver: ¿Cuál es la regla de aliasing estricta?
¿Funciona para referencias?
De acuerdo con los documentos de GCC 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 this
de funciones miembro:
void T::fn () __restrict__
En su artículo, Optimización de la memoria , Christer Ericson dice que aunque el restrict
no forma parte del estándar C ++, es compatible con muchos compiladores y recomienda su uso cuando está disponible:
restringir palabra clave
! Nuevo en el estándar ANSI / ISO C de 1999
! Aún no está en el estándar C ++, pero es compatible con muchos compiladores de C ++
! Solo una sugerencia, por lo tanto, puede que no haga nada y seguir cumpliendo
Un puntero (o referencia) con restricción restringida ...
! ... es básicamente una promesa para el compilador de que, para el alcance del puntero, solo se accederá al objetivo del puntero a través de ese puntero (y de los punteros copiados de él).
En los compiladores de C ++ que lo soportan probablemente debería comportarse igual que en C.
Consulte esta publicación SO para obtener más información: ¿ uso realista de la palabra clave C99 ''restringir''?
Tómate media hora para revisar el periódico de Ericson, es interesante y vale la pena el tiempo.
Editar
También encontré que el compilador AIX C / C ++ de IBM admite la palabra clave __restrict__
.
g ++ también parece apoyar esto ya que el siguiente programa compila limpiamente en g ++:
#include <stdio.h>
int foo(int * __restrict__ a, int * __restrict__ b) {
return *a + *b;
}
int main(void) {
int a = 1, b = 1, c;
c = foo(&a, &b);
printf("c == %d/n", c);
return 0;
}
También encontré un buen artículo sobre el uso de restrict
:
Desmitificar la palabra clave restringir
Edit2
Me encontré con un artículo que analiza específicamente el uso de restringir en los programas de C ++:
Load-hit-stores y la palabra clave __restrict
Además, Microsoft Visual C ++ también admite la palabra clave __restrict
.
Nada. Fue agregado al estándar C99.
No existe tal palabra clave en C ++. La lista de palabras clave de C ++ se puede encontrar en la sección 2.11 / 1 del estándar de lenguaje C ++. restrict
es una palabra clave en la versión C99 del lenguaje C y no en C ++.