teorema solapamiento señales que programacion nyquist geofisica eliminar ejemplos como c++ gcc optimization compiler-optimization strict-aliasing

c++ - solapamiento - ¿Por qué GCC y Clang no hacen esta optimización de aliasing?



que es el solapamiento de señales (4)

Creo que lo siguiente es legal C ++ (sin invocar a UB):

#include <new> struct A { int a; }; struct B : A { // int x; }; static A a; int g(B *b); int g(B *b) { a.a = 10; b->a++; return a.a; } int f(); int f() { auto p = new (&a) B{}; return g(p); }

porque (el global) a siempre se refiere a un objeto de tipo A (aunque es un subobjeto de un objeto B después de una llamada a f() ) y p apunta a un objeto de tipo B

Si marca a para tener una duración de almacenamiento static (como lo hice anteriormente), todos los compiladores que he probado aplicarán felizmente un alias estricto y lo optimizarán para devolver 10 .

Por otro lado, si marca g() con __attribute__((noinline)) o agrega una función h() que devuelve un puntero a a

A* h(); A* h() { return &a; }

los compiladores que he probado suponen que &a y el parámetro b pueden alias y recargar el valor.

Tengo un caso en el que un amigo emite un objeto de clase no base de tipo "Base" a un objeto de tipo de clase "Derivado", donde "Derivado" es una clase derivada de "Base" y solo agrega funciones, pero no datos. En el siguiente código, agregué un miembro de datos x a la clase derivada

struct A { int a; }; struct B : A { // int x; int x; }; A a; int g(B *b) { a.a = 10; b->a++; return a.a; }

Con el análisis de alias estricto activado, GCC (también Clang) siempre devuelve 10 , y no 11 , porque b nunca puede apuntar a a código bien definido. Sin embargo, si elimino B::x (como es el caso en el código de mi amigo), el código del ensamblador de salida de GCC no optimiza el acceso de retorno de aa y vuelve a cargar el valor de la memoria. Así que el código de mi amigo que llama a g "funciona" en GCC (como él pretendía) aunque creo que todavía tiene un comportamiento indefinido

g((B*)&a);

Entonces, en los mismos dos casos, GCC optimiza un caso y no optimiza el otro. ¿Es porque b puede entonces legalmente apuntar a a ? ¿O es porque GCC solo quiere no romper el código del mundo real?

Probé la respuesta que dice.

Si elimina B :: x, entonces B cumple con los requisitos de 9p7 para una clase de diseño estándar y el acceso se vuelve perfectamente bien definido porque los dos tipos son compatibles con el diseño, 9.2p17.

Con dos enums de diseño compatible

enum A : int { X, Y }; enum B : int { Z }; A a; int g(B *b) { a = Y; *b = Z; return a; }

La salida del ensamblador para g devuelve 1 , no 0 , aunque A y B son compatibles con el diseño (7.2p8).

Así que mi pregunta adicional es (citando una respuesta): "dos clases con exactamente el mismo diseño pueden considerarse" casi iguales "y quedan fuera de la optimización". . ¿Puede alguien proporcionar una prueba de esto para GCC o Clang?


Creo que su código es UB, ya que está desviando la referencia de un puntero que proviene de una conversión que viola las reglas de alias de tipo .

Ahora, si activa el indicador de alias estricto, está permitiendo que el compilador optimice el código para UB. Cómo usar este UB depende del compilador. Puedes ver las respuestas a esta pregunta .

Con respecto a gcc, la documentación para -fstrict-aliasing muestra que puede optimizar basándose en:

(...) se supone que un objeto de un tipo nunca reside en la misma dirección que un objeto de un tipo diferente, a menos que los tipos sean casi iguales.

No he podido encontrar una definición de "casi lo mismo", pero dos clases con exactamente el mismo diseño pueden considerarse "casi lo mismo" y quedan fuera de la optimización.


El comportamiento indefinido incluye el caso de que funcione, incluso si no debería.

De acuerdo con el uso estándar de esta unión, permite acceder a los campos de tipo y tamaño de los miembros de encabezado o de datos:

union Packet { struct Header { short type; short size; } header; struct Data { short type; short size; unsigned char data[MAX_DATA_SIZE]; } data; }

Esto se limita estrictamente a los sindicatos, pero muchos compiladores admiten que como tipo de extensión, siempre que el tipo "incompleto" se termine con una matriz de tamaño indefinido. Si elimina elementos no miembros extra estáticos de la clase secundaria, de hecho se vuelve trivial y compatible con el diseño, lo que permite crear alias.

struct A { int a; }; struct B { int a; //int x; }; A a; int g(B *b) { a.a = 10; b->a++; return a.a; }

Sin embargo, todavía realiza la optimización de aliasing. En su caso con la misma cantidad de miembros no estáticos, se supone que la clase más derivada es la misma que la clase base. Vamos a invertir el orden:

#include <vector> #include <iostream> struct A { int a; }; struct B : A { int x; }; B a; int g(A *b) { a.a = 10; b->a++; return a.a; } int main() { std::cout << g((A*)&a); }

Esto devuelve 11 como se esperaba, ya que B es claramente también una A, a diferencia del intento original. Vamos a jugar mas

struct A { int a; }; struct B : A { int foo() { return a;} };

No causaría optimización de alias, a menos que foo () sea virtual. Agregar un miembro no estático o constante a B daría como resultado una respuesta "10", agregando un constructor no trivial o una estática no.

PD. En segundo ejemplo

enum A : int { X, Y }; enum B : int { Z };

La compatibilidad de diseño entre esos dos aquí está definida por C ++ 14, y no son compatibles con el tipo subyacente (pero se pueden convertir). aunque algo como

enum A a = Y; enum B b = (B*)a;

puede producir un comportamiento indefinido, como si tratara de asignar un flotador con un valor arbitrario de 32 bits.


Si elimina B::x , entonces B cumple con los requisitos en 9p7 para una clase de diseño estándar y el acceso se vuelve perfectamente bien definido porque los dos tipos son compatibles con el diseño , 9.2p17 y los miembros tienen el mismo tipo.

Una clase de diseño estándar es una clase que:

  • no tiene miembros de datos no estáticos de tipo clase de diseño no estándar (o matriz de tales tipos) o referencia,
  • no tiene funciones virtuales (10.3) ni clases de base virtual (10.1),
  • tiene el mismo control de acceso (Cláusula 11) para todos los miembros de datos no estáticos,
  • no tiene clases de base de diseño no estándar,
  • o no tiene miembros de datos no estáticos en la clase más derivada y, como máximo, una clase base con miembros de datos no estáticos , o no tiene clases base con miembros de datos no estáticos, y
  • no tiene clases base del mismo tipo que el primer miembro de datos no estáticos.

Dos tipos de estructuras de diseño estándar son compatibles con el diseño si tienen el mismo número de miembros de datos no estáticos y los miembros de datos no estáticos correspondientes (en orden de declaración) tienen tipos compatibles con el diseño.