Reglas para usar la palabra clave restrict en C?
optimization memory (8)
¿Estás ejecutando Ubuntu de 32 o 64 bits? Si es de 32 bits, entonces necesita agregar -march=core2 -mfpmath=sse
(o cualquiera que sea su arquitectura de procesador), de lo contrario no usa SSE. En segundo lugar, para habilitar la vectorización con GCC 4.2, debe agregar la opción -ftree-vectorize
(a partir de 4.3 o 4.4, esto se incluye por defecto en -O3
). También podría ser necesario agregar -ffast-math
(u otra opción que proporcione semántica de coma flotante relajada) para permitir al compilador reordenar operaciones de punto flotante.
Además, agregue la -ftree-vectorizer-verbose=1
para ver si logra vectorizar el ciclo o no; esa es una manera fácil de verificar el efecto de agregar la palabra clave restrict.
Estoy tratando de comprender cuándo y cuándo no usar la palabra clave restrict
en C y en qué situaciones proporciona un beneficio tangible.
Después de leer " Desmitificar la palabra clave restringida " (que proporciona algunas reglas prácticas sobre el uso), tengo la impresión de que cuando se pasan punteros a una función, debe tener en cuenta la posibilidad de que los datos apuntados se superpongan (alias) con cualquier otro argumento que se pasa a la función. Dada una función:
foo(int *a, int *b, int *c, int n) {
for (int i = 0; i<n; ++i) {
b[i] = b[i] + c[i];
a[i] = a[i] + b[i] * c[i];
}
}
el compilador tiene que volver a cargar c
en la segunda expresión, porque tal vez b
y c
apuntan a la misma ubicación. También tiene que esperar a que b
se almacene antes de que pueda cargarse por la misma razón. Luego tiene que esperar a que se almacene y debe volver a cargar c
al comienzo del siguiente ciclo. Si llama a la función de esta manera:
int a[N];
foo(a, a, a, N);
entonces puedes ver por qué el compilador tiene que hacer esto. El uso de la restrict
le dice al compilador que nunca lo hará, de modo que puede soltar la carga redundante de c
y cargar a
antes de que b
se almacene.
Hasta ahora he llegado a la conclusión de que es una buena idea usar restrict
en punteros para pasar a funciones que no estarán en línea. Aparentemente, si el código está en línea, el compilador puede darse cuenta de que los punteros no se superponen.
Ahora aquí es donde las cosas empiezan a ser confusas para mí.
En el documento de Ulrich Drepper, " Lo que todo programador debería saber sobre la memoria ", hace la afirmación de que "a menos que se use restricción, todos los accesos al puntero son fuentes potenciales de aliasing" y da un ejemplo de código específico de una matriz de submatrices multiplicada donde usa restrict
Sin embargo, cuando compilo su código de ejemplo con o sin restrict
obtengo binarios idénticos en ambos casos. Estoy usando la gcc version 4.2.4 (Ubuntu 4.2.4-1ubuntu4)
Lo que no puedo entender en el siguiente código es si necesita ser reescrito para hacer un uso más extenso de restrict
, o si el análisis de alias en GCC es tan bueno que es capaz de descubrir que ninguno de los argumentos alias El uno al otro. Para fines puramente educativos, ¿cómo puedo hacer uso o no de restrict
materia en este código, y por qué?
Para restrict
compilado con:
gcc -DCLS=$(getconf LEVEL1_DCACHE_LINESIZE) -DUSE_RESTRICT -Wextra -std=c99 -O3 matrixMul.c -o matrixMul
Solo elimine -DUSE_RESTRICT
para no usar restrict
.
#include <stdlib.h>
#include <stdio.h>
#include <emmintrin.h>
#ifdef USE_RESTRICT
#else
#define restrict
#endif
#define N 1000
double _res[N][N] __attribute__ ((aligned (64)));
double _mul1[N][N] __attribute__ ((aligned (64)))
= { [0 ... (N-1)]
= { [0 ... (N-1)] = 1.1f }};
double _mul2[N][N] __attribute__ ((aligned (64)))
= { [0 ... (N-1)]
= { [0 ... (N-1)] = 2.2f }};
#define SM (CLS / sizeof (double))
void mm(double (* restrict res)[N], double (* restrict mul1)[N],
double (* restrict mul2)[N]) __attribute__ ((noinline));
void mm(double (* restrict res)[N], double (* restrict mul1)[N],
double (* restrict mul2)[N])
{
int i, i2, j, j2, k, k2;
double *restrict rres;
double *restrict rmul1;
double *restrict rmul2;
for (i = 0; i < N; i += SM)
for (j = 0; j < N; j += SM)
for (k = 0; k < N; k += SM)
for (i2 = 0, rres = &res[i][j],
rmul1 = &mul1[i][k]; i2 < SM;
++i2, rres += N, rmul1 += N)
for (k2 = 0, rmul2 = &mul2[k][j];
k2 < SM; ++k2, rmul2 += N)
for (j2 = 0; j2 < SM; ++j2)
rres[j2] += rmul1[k2] * rmul2[j2];
}
int main (void)
{
mm(_res, _mul1, _mul2);
return 0;
}
¿Puede ser que la optimización realizada aquí no dependa de que los punteros no tengan alias? A menos que precargue múltiples elementos mul2 antes de escribir el resultado en res2, no veo ningún problema de aliasing.
En la primera pieza de código que muestra, está bastante claro qué clase de alias puede ocurrir. Aquí no está tan claro.
Releyendo el artículo de Dreppers, él no dice específicamente que restringir podría resolver algo. Incluso hay esta frase:
{En teoría, la palabra clave restrictiva introducida en el lenguaje C en la revisión de 1999 debería resolver el problema. Los compiladores aún no se han puesto al día. La razón es principalmente que existe demasiado código incorrecto que podría confundir al compilador y provocar que genere código de objeto incorrecto.}
En este código, las optimizaciones del acceso a la memoria ya se han realizado dentro del algoritmo. La optimización residual parece hacerse en el código vectorizado presentado en el apéndice. Entonces, para el código presentado aquí, supongo que no hay diferencia, porque no se realiza ninguna optimización que dependa de restringir. Cada acceso al puntero es una fuente de alias, pero no todas las optimizaciones se basan en aliassing.
La optimización prematura es la raíz de todos los males, el uso de la palabra clave restrict debe limitarse al caso en el que esté estudiando y optimizando activamente, no se use donde sea que se pueda usar.
(No sé si usar esta palabra clave le da una ventaja significativa, en realidad. Es muy fácil para el programador equivocarse con este calificador ya que no hay aplicación, por lo que un optimizador no puede estar seguro de que el programador no "mienta". )
Cuando sabe que un puntero A es el único puntero a alguna región de la memoria, es decir, no tiene alias (es decir, cualquier otro puntero B será necesariamente desigual a A, B! = A), puede distinguir este hecho para el optimizador calificando el tipo de A con la palabra clave "restringir".
He escrito sobre esto aquí: http://mathdev.org/node/23 y traté de mostrar que algunos apuntadores restringidos son de hecho "lineales" (como se menciona en esa publicación).
Además, GCC 4.0.0-4.4 tiene un error de regresión que hace que la palabra clave restrict sea ignorada. Este error fue reportado como arreglado en 4.5 (sin embargo, perdí el número de error).
El problema con su código de ejemplo es que el compilador simplemente alineará la llamada y verá que no hay ningún alias posible en su ejemplo. Le sugiero que elimine la función main () y la compile con -c.
Es una pista para el optimizador de código. El uso de Restringir asegura que puede almacenar una variable de puntero en un registro de CPU y no tener que vaciar una actualización del valor del puntero a la memoria para que también se actualice un alias.
Si lo aprovecha o no, depende en gran medida de los detalles de implementación del optimizador y la CPU. Los optimizadores de código ya están fuertemente invertidos en la detección de no alias, ya que es una optimización tan importante. No debería tener problemas para detectar eso en tu código.
Si hay alguna diferencia, mover mm
a un DSO separado (de modo que gcc ya no sepa todo sobre el código de llamada) será la forma de demostrarlo.
Vale la pena señalar que las versiones recientes de clang
son capaces de generar código con un control de alias en tiempo de ejecución y dos rutas de código: una para casos donde hay un alias potencial y otra para el caso donde es obvio que no hay posibilidad de que .
Esto claramente depende de la extensión de los datos que apuntan a ser conspicuos para el compilador, como lo serían en el ejemplo anterior.
Creo que la principal justificación es que los programas hagan un uso intensivo de STL, y particularmente de <algorithm>
, donde es difícil o imposible introducir el __restrict
calificador __restrict
.
Por supuesto, todo esto se produce a expensas del tamaño del código, pero elimina un gran potencial para errores poco claros que podrían resultar para los punteros declarados como __restrict
no ser tan no superpuestos como pensaba el desarrollador.
Me sorprendería si GCC no hubiera obtenido esta optimización.