¿La palabra clave restrict proporciona beneficios significativos en gcc / g ++

¿Alguien ha visto números / análisis sobre si el uso de la palabra clave restrict C / C ++ en gcc / g ++ real proporciona algún impulso de rendimiento significativo en la realidad (y no solo en teoría)?

He leído varios artículos que recomiendan / menosprecian su uso, pero no he encontrado números reales que demuestren prácticamente argumentos de ambos lados.

EDITAR

Sé que restrict no es oficialmente parte de C ++, pero es compatible con algunos comstackdores y he leído un artículo de Christer Ericson, que recomienda encarecidamente su uso.

La palabra clave restrict hace una diferencia.

He visto mejoras del factor 2 y más en algunas situaciones (procesamiento de imágenes). La mayoría de las veces la diferencia no es tan grande. Como 10%.

Aquí hay un pequeño ejemplo que ilustra la diferencia. He escrito una transformación de matriz vectorial 4×4 muy básica * como prueba. Tenga en cuenta que tengo que forzar la función para que no esté en línea. De lo contrario, GCC detecta que no hay punteros de aliasing en mi código de referencia y restringir no hará una diferencia debido a la alineación.

Podría haber movido la función de transformación a un archivo diferente también.

 #include  #ifdef USE_RESTRICT #else #define __restrict #endif void transform (float * __restrict dest, float * __restrict src, float * __restrict matrix, int n) __attribute__ ((noinline)); void transform (float * __restrict dest, float * __restrict src, float * __restrict matrix, int n) { int i; // simple transform loop. // written with aliasing in mind. dest, src and matrix // are potentially aliasing, so the compiler is forced to reload // the values of matrix and src for each iteration. for (i=0; i 

Resultados: (en mi 2 Ghz Core Duo)

 nils@doofnase:~$ gcc -O3 test.c nils@doofnase:~$ time ./a.out real 0m2.517s user 0m2.516s sys 0m0.004s nils@doofnase:~$ gcc -O3 -DUSE_RESTRICT test.c nils@doofnase:~$ time ./a.out real 0m2.034s user 0m2.028s sys 0m0.000s 

Sobre el pulgar ejecución un 20% más rápida, en ese sistema.

Para mostrar cuánto depende de la architecture, he permitido que se ejecute el mismo código en una CPU integrada Cortex-A8 (se ajustó el recuento de bucles un poco porque no quiero esperar tanto):

 root@beagleboard:~# gcc -O3 -mcpu=cortex-a8 -mfpu=neon -mfloat-abi=softfp test.c root@beagleboard:~# time ./a.out real 0m 7.64s user 0m 7.62s sys 0m 0.00s root@beagleboard:~# gcc -O3 -mcpu=cortex-a8 -mfpu=neon -mfloat-abi=softfp -DUSE_RESTRICT test.c root@beagleboard:~# time ./a.out real 0m 7.00s user 0m 6.98s sys 0m 0.00s 

Aquí la diferencia es solo del 9% (el mismo comstackdor por cierto)

El artículo Desmitificando la palabra clave restrictiva se refiere al artículo Por qué el alias especificado por el progtwigdor es una mala idea (pdf) que dice que generalmente no ayuda y proporciona medidas para respaldar esto.

¿La palabra clave restrict proporciona beneficios significativos en gcc / g ++?

Puede reducir el número de instrucciones como se muestra en el siguiente ejemplo, así que úselo siempre que sea posible.

GCC 4.8 Linux x86-64 ejemplo

Entrada:

 void f(int *a, int *b, int *x) { *a += *x; *b += *x; } void fr(int *restrict a, int *restrict b, int *restrict x) { *a += *x; *b += *x; } 

Comstackr y descomstackr:

 gcc -g -std=c99 -O0 -c main.c 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

Conclusión: 3 instrucciones en lugar de 4 .

Por supuesto, las instrucciones pueden tener diferentes latencias , pero esto da una buena idea.

¿Por qué GCC pudo optimizar eso?

El código anterior fue tomado del ejemplo de Wikipedia que es muy esclarecedor.

Pseudo ensamblado para f :

 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 

Para fr :

 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 

¿Es realmente más rápido?

Ermmm … no por esta simple prueba:

 .text .global _start _start: mov $0x10000000, %rbx mov $x, %rdx mov $x, %rdi mov $x, %rsi loop: # START of interesting block mov (%rdx),%eax add %eax,(%rdi) mov (%rdx),%eax # Comment out this line. add %eax,(%rsi) # END ------------------------ dec %rbx cmp $0, %rbx jnz loop mov $60, %rax mov $0, %rdi syscall .data x: .int 0 

Y entonces:

 as -o ao aS && ld ao && time ./a.out 

en Ubuntu 14.04 AMD64 CPU Intel i5-3210M.

Confieso que todavía no entiendo las CPU modernas. Dejame saber si tu:

  • encontré un defecto en mi método
  • encontró un caso de prueba ensamblador donde se vuelve mucho más rápido
  • entender por qué no hubo diferencia

Probé este C-Progtwig. Sin restrict , tardó 12.640 segundos en completarse, con un límite de 12.516. Parece que puede ahorrar algo de tiempo.

Tenga en cuenta que los comstackdores de C ++ que permiten restrict palabra clave aún pueden ignorarla. Ese es el caso por ejemplo aquí .