2009-12-27 9 views
40

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

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

EDITAR

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

+9

cuestiones son Aliasing comúnmente considerado el motivo # 1 por el cual C/C++ es menos eficiente en muchas tareas computacionales que Fortran. Entonces, diría que cualquier característica que ayude a evitar el aliasing puede marcar una * gran * diferencia. – jalf

+0

posible duplicado de [Uso realista de la palabra clave 'restringir' C99?] (Http://stackoverflow.com/questions/745870/realistic-usage-of-the-c99-restrict-keyword) –

Respuesta

41

La palabra clave restrict hace la 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)

[email protected]:~$ gcc -O3 test.c 
[email protected]:~$ time ./a.out 

real 0m2.517s 
user 0m2.516s 
sys  0m0.004s 

[email protected]:~$ gcc -O3 -DUSE_RESTRICT test.c 
[email protected]:~$ time ./a.out 

real 0m2.034s 
user 0m2.028s 
sys  0m0.000s 

sobre el pulgar 20% más rápido de ejecución, en la que sistema.

Para mostrar lo mucho que depende de la arquitectura He dejado que el mismo código se ejecute en una CPU Cortex-A8 incrustado (ajustado el bucle contar un poco porque yo no quiero esperar tanto tiempo):

[email protected]:~# gcc -O3 -mcpu=cortex-a8 -mfpu=neon -mfloat-abi=softfp test.c 
[email protected]:~# time ./a.out 

real 0m 7.64s 
user 0m 7.62s 
sys  0m 0.00s 

[email protected]:~# gcc -O3 -mcpu=cortex-a8 -mfpu=neon -mfloat-abi=softfp -DUSE_RESTRICT test.c 
[email protected]:~# time ./a.out 

real 0m 7.00s 
user 0m 6.98s 
sys  0m 0.00s 

Aquí la diferencia es sólo el 9% (mismo compilador por cierto.)

+2

Buen trabajo. Hay un artículo sobre el uso de restringir en un procesador Cell aquí: http://cellperformance.beyond3d.com/articles/2006/05/demystifying-the-restrict-keyword.html que puede ser relevante para los beneficios específicos de arquitectura de discusión . – Clifford

+0

@Nils Pipenbrinck: ¿Por qué tiene que desactivar la alineación para la función? Parece una función terriblemente grande para el compilador alinearse automáticamente. –

+2

@Nils Pipenbrinck: Por cierto, Ulrich Drepper publicó el código para una matriz multiplicada superoptimizada como parte de su discusión sobre la optimización del uso de la memoria caché y la memoria. Está aquí: http://lwn.net/Articles/258188/. Su discusión de cada paso que pasó para llegar a esa solución está aquí: http://lwn.net/Articles/255364/. Pudo reducir el tiempo de ejecución en un 90% con respecto a un MM estándar. –

0

He probado this C-Program. Sin restrict tomó 12.640 segundos para completar, con restrict 12.516. Parece que puede guardar un poco de tiempo.

+23

Ese tipo de diferencia está en el ruido de medición ... –

+0

Esa diferencia es casi ciertamente insignificante, sin embargo, también debe declarar c como restringido ya que cada vez que se escribe c en el momento en que el compilador puede considerar que * a * by * inc pudieron haber sido cambiado – James

+0

En su ejemplo, el optimizador puede detectar que los parámetros no tienen aliasing. Intenta deshabilitar la línea y verás una mayor diferencia. –

0

Tenga en cuenta que los compiladores de C++ que permiten la palabra clave restrict aún pueden ignorarla. Ese es el caso, por ejemplo, here.

+0

En realidad, si lee la página, notará que, aunque se ignora el límite en C++ debido a un posible conflicto con las variables de usuario del mismo nombre, '__restrict__' es compatible con C++. –

+1

@Robert: E ignorado. La diferencia es solo que los identificadores con un doble guion bajo están reservados para el uso del sistema. Por lo tanto, un \ _ \ _ restrictivo \ _ \ _ no debe entrar en conflicto con ningún identificador declarado por el usuario. –

+0

@Martin: ¿Cómo sabes que se ignora? No está completamente claro en la documentación, parece que podrías leerlo de cualquier manera. –

6

el artículo Demystifying The Restrict Keyword se refiere al papel Why Programmer-specified Aliasing is a Bad Idea (pdf) que dice que por lo general no ayuda y proporciona mediciones que apoyen esta tesis.

+0

Existen muchos tipos de código en los que ofrece pocos beneficios, pero hay algunos en los que proporciona un gran beneficio. ¿Conoce algún documento que demuestre que el uso juicioso de "restringir" no ofrecería mayores beneficios de los que los compiladores pueden realizar a través del alias basado en tipos? – supercat

3

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

Se puede reducir el número de instrucciones como se muestra en el ejemplo a continuación, a fin de utilizarlo siempre que sea posible.

GCC 4.8 Linux x86-64 exmample

de 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, 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

Conclusión: 3 instrucciones en lugar de 4.

Por supuesto, las instrucciones can have different latencies, pero esto da una buena idea.

¿Por qué GCC pudo optimizar eso?

El código anterior fue tomado del Wikipedia example que es muy iluminador.

Pseudo montaje 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 sencilla 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 luego:

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.Déjeme saber si usted:

  • encontrado un fallo en mi método
  • encontró un caso de prueba ensamblador donde se convierte en mucho más rápido
  • entender por qué no había una diferencia