c gcc strict-aliasing

GCC y estricto alias entre matrices de un mismo tipo



strict-aliasing (2)

En mi humilde opinión, la norma no permite que las matrices de tamaño definido se superpongan al mismo tiempo (*). El borrador n1570 dice en 6.2.7 Tipo compatible y tipo compuesto (énfasis mío):

§2 Todas las declaraciones que se refieran al mismo objeto o función tendrán un tipo compatible; de lo contrario, el comportamiento es indefinido.

§3 Un tipo compuesto puede construirse a partir de dos tipos que son compatibles; es un tipo que es compatible con los dos tipos y cumple las siguientes condiciones:

  • Si ambos tipos son tipos de matriz, se aplican las siguientes reglas:
    • Si un tipo es una matriz de tamaño constante conocido , el tipo compuesto es una matriz de ese tamaño .
      ...

Como un objeto debe tener acceso a su valor almacenado solo por una expresión lvalue que tenga un tipo compatible (lectura simplificada de 6.5 Expresiones §7), no puede crear alias matrices de diferentes tamaños, ni puede tener matrices del mismo tamaño que se superpongan. Por lo tanto, en la función g, p y q deben apuntar a la misma matriz o a matrices no superpuestas, lo que permite la optimización.

Para las funciones f y k, tengo entendido que la optimización se permitiría según el estándar pero no ha sido implementada por los desarrolladores. Debemos recordar que tan pronto como uno de los parámetros es un puntero simple, se le permite apuntar a cualquier elemento de la otra matriz y no se puede realizar ninguna optimización. Así que creo que la ausencia de optimización es solo un ejemplo de la famosa regla de UB: cualquier cosa puede suceder, incluido el resultado esperado .

Contexto

El "alias estricto", que lleva el nombre de la optimización GCC, es una suposición por parte del compilador de que no se accederá a un valor en la memoria a través de un lvalor de un tipo (el "tipo declarado") muy diferente del tipo con el que se escribió el valor ( el "tipo efectivo"). Este supuesto permite transformaciones de código que serían incorrectas si se tuviera en cuenta la posibilidad de que escribir en un puntero para float podría modificar una variable global de tipo int .

Tanto GCC como Clang, que extraen el mayor significado de una descripción estándar llena de esquinas oscuras , y que tienen un sesgo para el rendimiento del código generado en la práctica, suponen que un puntero al primer miembro int de una struct thing no es un alias de un puntero el primer miembro int de un struct object :

struct thing { int a; }; struct object { int a; }; int e(struct thing *p, struct object *q) { p->a = 1; q->a = 2; return p->a; }

Tanto GCC como Clang infer que la función siempre devuelve 1, es decir, que q no pueden ser alias para la misma ubicación de memoria:

e: movl $1, (%rdi) movl $1, %eax movl $2, (%rsi) ret

Mientras uno esté de acuerdo con el razonamiento de esta optimización, no debería sorprender que p->t[3] y q->t[2] también se asuman como valores desunidos en el siguiente fragmento de código (o más bien, que el que llama causa UB si tienen alias):

struct arr { int t[10]; }; int h(struct arr *p, struct arr *q) { p->t[3] = 1; q->t[2] = 2; return p->t[3]; }

GCC optimiza la función anterior h :

h: movl $1, 12(%rdi) movl $1, %eax movl $2, 8(%rsi) ret

Hasta ahora todo bien, siempre y cuando uno vea que p->a p->t[3] alguna manera accede a una struct thing completa (resp. struct arr ), es posible argumentar que hacer un alias de ubicaciones rompería las reglas Establecido en 6.5: 6-7. Un argumento de que este es el enfoque de GCC es este mensaje , parte de un largo hilo que también trató el papel de los sindicatos en las reglas estrictas de alias.

Pregunta

Tengo dudas, sin embargo, sobre el siguiente ejemplo, en el que no hay ninguna struct :

int g(int (*p)[10], int (*q)[10]) { (*p)[3] = 1; (*q)[4] = 2; return (*p)[3]; }

Las versiones 4.4.7 de GCC a través de la instantánea de la versión 7 actual en el útil sitio web de Matt Godbolt optimizan la función g como si (*p)[3] y (*q)[4] no pudieran hacer alias (o más bien, como si el programa hubiera invocado UB si lo hicieran):

g: movl $1, 12(%rdi) movl $1, %eax movl $2, 16(%rsi) ret

¿Hay alguna lectura del estándar que justifique este enfoque muy estricto para establecer un alias estricto? Si la optimización de GCC aquí se puede justificar, ¿los argumentos se aplicarían también a la optimización de las funciones f y k , que no están optimizadas por GCC?

int f(int (*p)[10], int (*q)[9]) { (*p)[3] = 1; (*q)[3] = 2; return (*p)[3]; } int k(int (*p)[10], int (*q)[9]) { (*p)[3] = 1; (*q)[2] = 2; return (*p)[3]; }

Estoy dispuesto a abordar esto con los desarrolladores de GCC, pero primero debo decidir sin informar sobre un error de corrección para la función g o una optimización perdida para f y k .


En:

int g(int (*p)[10], int (*q)[10]) { (*p)[3] = 1; (*q)[4] = 2; return (*p)[3]; }

*p y *q son valores de tipo de matriz; Si se pueden superponer, el acceso a ellos se rige por la sección 6.5, párrafo 7 (la llamada "regla estricta de aliasing"). Sin embargo, dado que su tipo es el mismo, no presenta un problema para este código. Sin embargo, la norma es notablemente vaga con respecto a una serie de inquietudes relevantes que se requerirían para dar una respuesta integral a esta pregunta, como:

  • ¿ (*p) y (*q) realmente requieren "acceso" (como se usa el término en 6.5p7) a las matrices a las que apuntan? Si no lo hacen, es tentador considerar que las expresiones (*p)[3] y (*q)[4] degradan esencialmente en aritmética de punteros y en la desreferencia de dos int * s que pueden ser claramente alias. (Este no es un punto de vista totalmente irrazonable; 6.5.2.1 . La creación de subíndices de arrays dice que una de las expresiones tendrá un tipo "puntero para completar el tipo de objeto", la otra expresión tendrá un tipo entero y el resultado tendrá un tipo "" '''' - por lo que el valor de la matriz necesariamente se ha degradado a un puntero según las reglas de conversión habituales; la única pregunta es si se accedió a la matriz antes de que se produjera la conversión)

  • Sin embargo, para defender la opinión de que (*p)[3] es puramente equivalente a *((int *)p + 3) , tendríamos que demostrar que (*p)[3] no requiere evaluación de (*p) , o que si lo hace, el acceso no tiene un comportamiento indefinido (o un comportamiento definido pero no deseado). No creo que haya ninguna justificación en la redacción precisa de la norma para permitir que (*p) no se evalúe; esto implica que la expresión (*p) no debe tener un comportamiento indefinido si el comportamiento de (*p)[3] está definido. Entonces, la pregunta realmente se reduce a si *p y *q tienen un comportamiento definido si se refieren a matrices parcialmente superpuestas del mismo tipo, y de hecho si es posible que puedan hacerlo simultáneamente.

Para la definición del operador * , la norma dice:

Si apunta a un objeto, el resultado es un lvalue que designa el objeto.

  • ¿Esto significa que el puntero debe apuntar al inicio del objeto? (Parece probable que esto es lo que se quiere decir). ¿Se debe haber establecido el objeto de alguna manera antes de poder acceder a él (y el establecimiento de un objeto no establece ningún objeto superpuesto)? Si ambos son el caso, *p y *q no se pueden superponer, ya que establecer uno u otro objeto invalidaría el otro, y así (*p)[3] y (*q)[4] no pueden hacer alias.

El problema es que no hay una guía adecuada para estas preguntas. En mi opinión, debería adoptarse un enfoque conservador: no asuma que este tipo de alias es legal.

En particular, la expresión "tipo efectivo" en 6.5 sugiere un medio por el cual se puede establecer un objeto de un tipo particular. Parece una buena apuesta que esto pretende ser definitivo; es decir, que no puede establecer un objeto que no sea estableciendo su tipo efectivo (incluido el hecho de que tenga un tipo declarado), y que el acceso de otros tipos esté restringido; Además, establecer un objeto no establece ningún objeto superpuesto existente (para ser claro, esto es extrapolación, no la redacción real). Por lo tanto, si (*p)[3] y (*q)[4] podrían obtener un alias, entonces p o q no apuntan a un objeto, y por lo tanto uno de los dos *p *q tiene un comportamiento indefinido.