Entender restringir calificador por ejemplos
c99 strict-aliasing (1)
El comportamiento de la palabra clave restrict
se define en C99 por 6.7.3.1:
Sea D una declaración de un identificador ordinario que proporciona un medio para designar un objeto P como un puntero de restricción calificada al tipo T.
Si D aparece dentro de un bloque y no tiene una clase de almacenamiento externa, sea B el bloque. Si D aparece en la lista de declaraciones de parámetros de una definición de función, sea que B denota el bloque asociado. De lo contrario, digamos que B denota el bloque de main (o el bloque de cualquier función que se llame al inicio del programa en un entorno independiente).
En lo que sigue, se dice que una expresión de puntero E se basa en el objeto P si (en algún punto de la secuencia en la ejecución de B antes de la evaluación de E) modifica P para apuntar a una copia del objeto de matriz al que apuntaba anteriormente cambiaría el valor de E.119) Tenga en cuenta que '''' basado '''' se define solo para expresiones con tipos de puntero.
Durante cada ejecución de B, sea L cualquier valor l que tenga & L basado en P. Si se usa L para acceder al valor del objeto X que designa, y X también se modifica (por cualquier medio), entonces se aplican los siguientes requisitos : T no será constante. Cada otro lvalor utilizado para acceder al valor de X también tendrá su dirección basada en P. Se considerará que cada acceso que modifica X también modifica P, a los efectos de esta subcláusula. Si a P se le asigna el valor de una expresión de puntero E que se basa en otro objeto de puntero restringido P2, asociado con el bloque B2, entonces la ejecución de B2 comenzará antes de la ejecución de B, o la ejecución de B2 finalizará antes de asignación. Si estos requisitos no se cumplen, entonces el comportamiento no está definido.
Como casi todos los demás, me cuesta entender todas las complejidades de esta definición. Como respuesta a esta pregunta, me gustaría ver una serie de buenos ejemplos, para cada requisito en el cuarto párrafo, de usos que violarían el requisito. Este artículo:
hace un buen trabajo al presentar las reglas en términos de "un compilador puede asumir ..."; expandir ese patrón y vincular las suposiciones que el compilador puede hacer, y cómo no pueden mantenerse, con cada ejemplo sería genial.
A continuación, me referiré a los casos de uso del artículo de Sun vinculado a la pregunta.
El caso (relativamente) obvio sería el caso mem_copy (), que se incluye en la segunda categoría de casos de uso en el documento de Sun (la función f1()
). Digamos que tenemos las siguientes dos implementaciones:
void mem_copy_1(void * restrict s1, const void * restrict s2, size_t n);
void mem_copy_2(void * s1, const void * s2, size_t n);
Como sabemos que no hay superposición entre las dos matrices apuntadas por s1 y s2, el código para la primera función sería sencillo:
void mem_copy_1(void * restrict s1, const void * restrict s2, size_t n)
{
// naively copy array s2 to array s1.
for (int i=0; i<n; i++)
s1[i] = s2[i];
return;
}
s2 = ''....................1234567890abcde'' <- s2 before the naive copy
s1 = ''1234567890abcde....................'' <- s1 after the naive copy
s2 = ''....................1234567890abcde'' <- s2 after the naive copy
OTOH, en la segunda función, puede haber una superposición. En este caso, debemos comprobar si la matriz de origen está ubicada antes del destino o viceversa, y elegir los límites del índice de bucle en consecuencia.
Por ejemplo, digamos s1 = 100
y s2 = 105
. Luego, si n=15
, después de la copia, la matriz s1
recién copiada superará los primeros 10 bytes de la matriz s2
origen. Tenemos que asegurarnos de que primero copiamos los bytes inferiores.
s2 = ''.....1234567890abcde'' <- s2 before the naive copy
s1 = ''1234567890abcde.....'' <- s1 after the naive copy
s2 = ''.....67890abcdeabcde'' <- s2 after the naive copy
Sin embargo, si, s1 = 105
y s2 = 100
, al escribir primero los bytes inferiores se sobrepasarán los últimos 10 bytes del origen s2
, y terminamos con una copia errónea.
s2 = ''1234567890abcde.....'' <- s2 before the naive copy
s1 = ''.....123451234512345'' <- s1 after the naive copy - not what we wanted
s2 = ''123451234512345.....'' <- s2 after the naive copy
En este caso, primero debemos copiar los últimos bytes de la matriz, posiblemente retrocediendo. El código se verá algo como:
void mem_copy_2(void *s1, const void *s2, size_t n)
{
if (((unsigned) s1) < ((unsigned) s2))
for (int i=0; i<n; i++)
s1[i] = s2[i];
else
for (int i=(n-1); i>=0; i--)
s1[i] = s2[i];
return;
}
Es fácil ver cómo el modificador de restrict
da la oportunidad de mejorar la optimización de la velocidad, eliminando el código adicional y una decisión en caso de que no sea así.
Al mismo tiempo, esta situación es peligrosa para el programador imprudente, que pasa las matrices superpuestas a la función restrict
. En este caso, no hay guardas para asegurar la copia correcta de la matriz. Dependiendo de la ruta de optimización elegida por el compilador, el resultado no está definido.
El primer caso de uso (la función init()
) puede verse como una variación del segundo, descrito anteriormente. Aquí, se crean dos matrices con una sola llamada de asignación de memoria dinámica.
Designar los dos punteros como restrict
permite la optimización en la cual el orden de las instrucciones importaría de otra manera. Por ejemplo, si tenemos el código:
a1[5] = 4;
a2[3] = 8;
entonces el optimizador puede reordenar estas declaraciones si lo encuentra útil.
OTOH, si los punteros no están restrict
, entonces es importante que la primera asignación se realice antes que la segunda. ¡Esto se debe a que existe la posibilidad de que a1[5]
y a2[3]
sean en realidad la misma ubicación de memoria! Es fácil ver que cuando este es el caso, entonces el valor final debería ser 8. Si reordenamos las instrucciones, ¡entonces el valor final será 4!
Nuevamente, si se asignan punteros no disjuntos a este código supuesto restrict
, el resultado es indefinido.