"Si un trabajador quiere hacer bien su trabajo, primero debe afilar sus herramientas." - Confucio, "Las Analectas de Confucio. Lu Linggong"
Página delantera > Programación > La oscura palabra clave "restringir" en C

La oscura palabra clave "restringir" en C

Publicado el 2024-11-04
Navegar:954

The Obscure “restrict” Keyword in C

Introducción

Entre otras cosas, C99 agregó la palabra clave restrict como una forma para que un programador especifique que un puntero es el único puntero a un objeto determinado en un alcance y, en consecuencia, le da al compilador una "pista". ”que puede realizar optimizaciones adicionales al acceder al objeto a través de ese puntero.

El problema

Para ilustrar el problema que la restricción debía resolver, considere una función como:

void update_ptrs( int *p, int *q, int const *v ) {
  *p  = *v;
  *q  = *v;
}

para lo cual el compilador generará código x86-64 como:

mov eax, [rdx]  ; tmp = *v   // 1
add [rdi], eax  ; *p  = tmp
mov eax, [rdx]  ; tmp = *v   // 3
add [rsi], eax  ; *q  = tmp

Quizás te preguntes por qué genera la línea 3, ya que parece redundante con la línea 1. El problema es que el compilador no puede saber que no hiciste algo como esto:

int x = 1, v = 2;
update_ptrs( &v, &x, &v );   // x = 5, v = 4

En update_ptrs(), p y v alias el mismo int, por lo que el compilador debe ir a lo seguro y asumir que el valor de *v puede cambiar entre lecturas, de ahí la instrucción mov adicional.

En general, los punteros en C confunden la optimización ya que el compilador no puede saber si dos punteros tienen alias entre sí. En código crítico para el rendimiento, eliminar lecturas de memoria podría ser una gran ganancia si el compilador pudiera hacerlo de forma segura.

La solución

Para resolver el problema antes mencionado, se agregó restricción a C para permitirle especificar que un puntero dado es el únicamente puntero a un objeto en el alcance del puntero, es decir, ningún otro puntero en el mismo alcance alias él.

Para usar restringir, lo insertas entre el * y el nombre del puntero en una declaración. Un update_ptrs() reescrito para usar restringir sería:

void update_ptrs_v2( int *restrict p, int *restrict q,
                     int const *restrict v ) {
  *p  = *v;
  *q  = *v;
}

(Leer de derecha a izquierda, por ejemplo, v es un puntero restringido a un int constante; o use cdecl.)

Al agregar restricción, el compilador ahora puede generar código como:

mov eax, [rdx]  ; tmp = *v
add [rdi], eax  ; *p  = tmp
add [rsi], eax  ; *q  = tmp

Ahora, el compilador pudo omitir la línea 3 anterior de la instrucción mov adicional.

Quizás el ejemplo más conocido en el que se utiliza restringir es la función de biblioteca estándar memcpy(). Es la forma más rápida de copiar una porción de memoria si las direcciones de origen y de destino no se superponen. La función memmove(), un poco más lenta, existe para usarse cuando las direcciones se superponen.

Escollos

El uso indebido de restringir da como resultado un comportamiento indefinido, por ejemplo, al pasar punteros que hacen alias entre sí para update_ptrs_v2() o memcpy(). En algunos casos, el compilador puede advertirle, pero no en todos los casos, así que no confíe en que el compilador detecte usos indebidos.

Tenga en cuenta que la restricción es para un alcance determinado. Asignar un puntero restringido a otro en el mismo ámbito da como resultado un comportamiento indefinido:

void f( int *restrict d, int *restrict s ) {
  int *restrict p = s;    // undefined behavior

Sin embargo, puedes asignar un puntero restringido a un puntero no restringido sin problemas:

void f( int *restrict d, int *restrict s ) {
  int *p = s;             // OK

Aunque p no tiene restricciones, el compilador aún puede realizar las mismas optimizaciones.

También está bien asignar un puntero restringido en un ámbito interno a otro en un ámbito externo (pero no al revés):

void f( int *restrict d, int *restrict s ) {
  {                       // inner scope
    int *restrict p = s;  // OK
    // ...
    s = p;                // undefined behavior
  }
}

Cuándo (y cuándo no) usar restringir

Primero, definitivamente deberías perfilar tu código (y tal vez incluso mirar el código ensamblador generado) para ver si el uso de restringir realmente genera una mejora significativa en el rendimiento para justificar el riesgo de posibles errores. Diagnosticar errores causados ​​por el mal uso de restringir es muy difícil de hacer.

En segundo lugar, si el uso de restringir se limita a la implementación de una función donde la memoria a la que se accede a través de punteros restringidos fue asignada por usted, entonces es más seguro. Por ejemplo, dado:

void safer( unsigned n ) {
  n  = n % 2 != 0;  // make even by rounding up
  int *const array = malloc( n * sizeof(unsigned) );
  unsigned *restrict half_1st = array;
  unsigned *restrict half_2nd = array   n/2;
  // ...
  free( array );
}

el código podría operar en la primera y segunda mitad de la matriz de forma segura porque no se superponen (suponiendo que nunca acceda a half_1st[n/2] o más allá).

En tercer lugar, si se usa restricción en los parámetros de una función, entonces es potencialmente menos segura. Por ejemplo, compare safer() con update_ptrs_v2() donde la persona que llama controla los punteros. Por lo que ustedes saben, la persona que llamó se equivocó y pasó indicadores de ese alias.

Misceláneas

Solo los punteros a objetos (o nulos) pueden calificarse con restricción:

restrict int x;       // error: can't restrict object
int restrict *p;      // error: pointer to restrict object
int (*restrict f)();  // error: pointer-to-function

Puedes usar restringir para los miembros de la estructura, por ejemplo:

struct node {
   void *restrict data;
   struct node *restrict left;
   struct node *restrict right;
};

dice que los datos serán el único puntero a esos datos y que la izquierda y la derecha nunca apuntarán al mismo nodo. Sin embargo, el uso de restricciones para miembros de estructuras es muy poco común.

Por último, C no tiene restricción. ¿Por qué no? Hay una respuesta larga, pero la versión TL;DR es la siguiente:

  • Puede ser una fuente de errores difíciles de encontrar que el comité C no quiso importar desde C.
  • El mayor uso de punteros por parte de C, por ejemplo, esto, hace que el uso de restringir de forma segura sea aún más difícil.

Sin embargo, muchos compiladores tienen __restrict__ como extensión.

Conclusión

En casos limitados, el uso de restricción puede generar mejoras en el rendimiento, pero existen varios inconvenientes importantes. Si estás pensando en utilizar la restricción, primero perfila tu código.

Úsalo sabiamente.

Declaración de liberación Este artículo se reproduce en: https://dev.to/pauljlucas/the-obscure-restrict-keyword-in-c-2541 Si hay alguna infracción, comuníquese con [email protected] para eliminarla.
Último tutorial Más>

Descargo de responsabilidad: Todos los recursos proporcionados provienen en parte de Internet. Si existe alguna infracción de sus derechos de autor u otros derechos e intereses, explique los motivos detallados y proporcione pruebas de los derechos de autor o derechos e intereses y luego envíelos al correo electrónico: [email protected]. Lo manejaremos por usted lo antes posible.

Copyright© 2022 湘ICP备2022001581号-3