c++ - compilador - ¿La palabra clave restrict proporciona beneficios significativos en gcc/g++
gcc wikipedia (5)
¿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;
}
Compilar y descompilar:
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 a.o a.S && ld a.o && 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
¿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 compiladores y he leído un artículo de Christer Ericson, que recomienda encarecidamente su uso.
El artículo cellperformance.beyond3d.com/articles/2006/05/… refiere al artículo Por qué el alias especificado por el programador es una mala idea (pdf) que dice que generalmente no ayuda y proporciona medidas para respaldar esto.
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 4x4 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 <math.h>
#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<n; i++)
{
dest[0] = src[0] * matrix[0] + src[1] * matrix[1] +
src[2] * matrix[2] + src[3] * matrix[3];
dest[1] = src[0] * matrix[4] + src[1] * matrix[5] +
src[2] * matrix[6] + src[3] * matrix[7];
dest[2] = src[0] * matrix[8] + src[1] * matrix[9] +
src[2] * matrix[10] + src[3] * matrix[11];
dest[3] = src[0] * matrix[12] + src[1] * matrix[13] +
src[2] * matrix[14] + src[3] * matrix[15];
src += 4;
dest += 4;
}
}
float srcdata[4*10000];
float dstdata[4*10000];
int main (int argc, char**args)
{
int i,j;
float matrix[16];
// init all source-data, so we don''t get NANs
for (i=0; i<16; i++) matrix[i] = 1;
for (i=0; i<4*10000; i++) srcdata[i] = i;
// do a bunch of tests for benchmarking.
for (j=0; j<10000; j++)
transform (dstdata, srcdata, matrix, 10000);
}
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 arquitectura, 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 compilador por cierto)
Probé this C-Programa. 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 compiladores de C ++ que permiten restrict
palabra clave aún pueden ignorarla. Ese es el caso por ejemplo here .