c++ - ¿Por qué la optimización mata esta función?
optimization endianness (3)
En C ++, se asume que los argumentos del puntero no son alias (excepto char*
) si apuntan a tipos fundamentalmente diferentes ( reglas de "aliasing estricto" ). Esto permite algunas optimizaciones.
Aquí, u64 tmp
nunca se modifica como u64
.
Un contenido de u32*
se modifica pero puede no estar relacionado con '' u64 tmp
'', por lo que puede verse como nop
para u64 tmp
.
Hace poco tuvimos una conferencia en la universidad sobre la programación de especiales en varios idiomas.
El conferenciante anotó la siguiente función:
inline u64 Swap_64(u64 x)
{
u64 tmp;
(*(u32*)&tmp) = Swap_32(*(((u32*)&x)+1));
(*(((u32*)&tmp)+1)) = Swap_32(*(u32*) &x);
return tmp;
}
Si bien entiendo totalmente que este es también un estilo realmente malo en términos de legibilidad, su punto principal fue que esta parte del código funcionó bien en el código de producción hasta que permitieron un alto nivel de optimización. Entonces, el código simplemente no haría nada.
Dijo que todas las asignaciones a la variable tmp
serían optimizadas por el compilador. Pero, ¿por qué sucedería esto?
Entiendo que hay circunstancias en las que las variables deben declararse volátiles para que el compilador no las toque aunque crea que nunca las lee o las escribe, pero no sabría por qué ocurriría esto aquí.
Este código infringe las reglas de alias estrictas que hacen que sea ilegal acceder a un objeto a través de un puntero de un tipo diferente, aunque se permite el acceso a través de * char **. El compilador puede suponer que los punteros de diferentes tipos no apuntan a la misma memoria y optimizan en consecuencia. También significa que el código invoca un comportamiento indefinido y realmente podría hacer cualquier cosa.
Una de las mejores referencias para este tema es Entender el alias estricto y podemos ver que el primer ejemplo está en una línea similar al código del OP:
uint32_t swap_words( uint32_t arg )
{
uint16_t* const sp = (uint16_t*)&arg;
uint16_t hi = sp[0];
uint16_t lo = sp[1];
sp[1] = hi;
sp[0] = lo;
return (arg);
}
El artículo explica que este código infringe reglas estrictas de aliasing ya que sp
es un alias de arg
pero tienen diferentes tipos y dice que aunque se compilará, es probable que arg
no se swap_words
después de que swap_words
regrese. Aunque con pruebas simples, no puedo reproducir ese resultado ni con el código anterior ni con el código OP, pero eso no significa nada, ya que este es un comportamiento indefinido y, por lo tanto, no es predecible.
El artículo continúa hablando de muchos casos diferentes y presenta varias soluciones de trabajo que incluyen el tipeo a través de una unión, que está bien definido en C99 1 y puede estar indefinido en C ++, pero en la práctica es compatible con la mayoría de los compiladores principales, por ejemplo aquí es la referencia de gcc en el tipo de juego de palabras . El hilo anterior Propósito de las Uniones en C y C ++ entra en los detalles sangrientos. Aunque hay muchos hilos sobre este tema, este parece hacer el mejor trabajo.
El código para esa solución es el siguiente:
typedef union
{
uint32_t u32;
uint16_t u16[2];
} U32;
uint32_t swap_words( uint32_t arg )
{
U32 in;
uint16_t lo;
uint16_t hi;
in.u32 = arg;
hi = in.u16[0];
lo = in.u16[1];
in.u16[0] = lo;
in.u16[1] = hi;
return (in.u32);
}
A modo de referencia, la sección pertinente del proyecto de norma C99 sobre alias estrictos es el párrafo 6.5
expresiones, que dice:
Un objeto debe tener su valor almacenado al que se accede solo mediante una expresión lvalue que tiene uno de los siguientes tipos: 76)
- un tipo compatible con el tipo efectivo del objeto,
- una versión calificada de un tipo compatible con el tipo de objeto efectivo,
- un tipo que es el tipo firmado o no firmado correspondiente al tipo efectivo del objeto,
- un tipo que es el tipo firmado o sin firmar correspondiente a una versión calificada del tipo efectivo del objeto,
- un tipo agregado o de unión que incluye uno de los tipos mencionados anteriormente entre sus miembros (incluido, recursivamente, un miembro de un subaggregado o sindicato contenido), o
- un tipo de personaje.
y la nota al pie 76 dice:
La intención de esta lista es especificar las circunstancias en las que un objeto puede o no tener alias.
y la sección relevante del borrador del estándar C ++ es 3.10
Lvalues and rvalues paragraph 10
El artículo Tipo-juego de palabras y alias estricto brinda una introducción más suave pero menos completa al tema, y C99 revisited brinda un análisis profundo de C99 y aliasing y no es una lectura ligera. ¿Esta respuesta a Acceder al miembro de la unión inactiva no está definida? repasa los detalles turbios de los juegos de palabras a través de una unión en C ++ y tampoco es una lectura ligera.
Notas al pie:
- Citando el comment de Pascal Cuoq: [...] C99, que inicialmente estaba torpemente redactado, pareciendo hacer indefinibles los juegos de palabras a través de los sindicatos. En realidad, el tipeo de tipos a través de los sindicatos es legal en C89, legal en C11, y fue legal en C99 todo el tiempo, aunque el comité tardó hasta 2004 en corregir la redacción incorrecta y la posterior publicación de TC3. open-std.org/jtc1/sc22/wg14/www/docs/dr_283.htm
g ++ (Ubuntu / Linaro 4.8.1-10ubuntu9) 4.8.1:
> g++ -Wall -std=c++11 -O0 -o sample sample.cpp
> g++ -Wall -std=c++11 -O3 -o sample sample.cpp
sample.cpp: In function ‘uint64_t Swap_64(uint64_t)’:
sample.cpp:10:19: warning: dereferencing type-punned pointer will break strict-aliasing rules [-Wstrict-aliasing]
(*(uint32_t*)&tmp) = Swap_32(*(((uint32_t*)&x)+1));
^
sample.cpp:11:54: warning: dereferencing type-punned pointer will break strict-aliasing rules [-Wstrict-aliasing]
(*(((uint32_t*)&tmp)+1)) = Swap_32(*(uint32_t*) &x);
^
Clang 3.4 no advierte en ningún nivel de optimización, que es curioso ...