¿Por qué la optimización elimina esta función?


Recientemente tuvimos una conferencia en la universidad sobre programas especiales en varios idiomas.

El profesor escribió 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 también es un estilo muy 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 habilitaron un alto nivel de optimización. Entonces, el código no haría nada.

Dijo que todas las asignaciones a la variable tmp sería optimizado por el compilador. ¿Pero por qué pasaría esto?

Entiendo que hay circunstancias donde las variables necesitan ser declaradas volátiles para que el compilador no las toque incluso si piensa que nunca se leen o escriben, pero no sabría por qué esto sucedería aquí.

Author: Wai Ha Lee, 2014-01-04

3 answers

Este código viola las estrictas reglas de alias que hacen ilegal acceder a un objeto a través de un puntero de un tipo diferente, aunque se permite el acceso a través de un *char**. El compilador puede asumir 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 Comprensión Strict Aliasing y podemos ver que el primer ejemplo es 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 viola 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 modifique después de que swap_words regrese. Aunque con pruebas simples, no puedo reproducir ese resultado con el código anterior ni con el código OPs, pero eso no significa nada ya que este es comportamiento indefinido y por lo tanto no predecible.

El artículo continúa hablando de muchos casos diferentes y presenta varias soluciones de trabajo que incluyen juego de tipo a través de una unión, que está bien definida en C991 y puede ser indefinido en C++ pero en la práctica es soportado por la mayoría de los compiladores principales, por ejemplo aquí está la referencia de gcc sobre el juego de tipos. El hilo anterior Propósito de las Uniones en C y C++ entra en los detalles sangrientos. Aunque hay muchos hilos sobre este tema, esto 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);
}

Para referencia, la sección pertinente de C99 draft standard on strict aliasing es 6.5 Expresiones párrafo 7 que dice:

Un objeto tendrá acceso a su valor almacenado solo por una expresión lvalue que tenga 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 efectivo del objeto,

- un tipo que es el tipo firmado o sin firmar correspondiente al tipo efectivo del objeto,

- un tipo que es el tipo firmado o sin firmar correspondiente a una versión calificada de la tipo efectivo del objeto,

- an tipo agregado o unión que incluye uno de los tipos antes mencionados entre sus miembros (incluyendo, recursivamente, un miembro de un subagregado o sindicato contenido), o

- un tipo de carácter.

Y La nota 76 dice:

La intención de esta lista es especificar las circunstancias en las que un objeto puede o no ser alias.

Y la sección relevante del C++ draft standard es 3.10 Lvalues y rvalues párrafo 10

El artículo Type-punning and strict-aliasing da una introducción más suave pero menos completa al tema y C99 revisited da un análisis profundo de C99 y aliasing y no es una lectura ligera. Esta respuesta a Accediendo a miembro del sindicato inactivo - undefined? repasa los detalles embarrados del juego de palabras a través de una unión en C++ y no es lectura ligera bien.


Notas al pie:

  1. Citando comentario de Pascal Cuoq: [...] C99 que fue inicialmente torpemente redactada, pareciendo hacer indefinido el juego de tipo a través de uniones. En realidad, el juego de tipo aunque los sindicatos es legal en C89, legal en C11, y fue legal en C99 todo el tiempo, aunque tomó hasta 2004 para el comité para fijar la redacción incorrecta, y el lanzamiento posterior de TC3. open-std.org/jtc1/sc22/wg14/www/docs/dr_283.htm
 41
Author: Shafik Yaghmour,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 12:25:55

En C++, los argumentos puntero no se asumen como alias (excepto char*) si apuntan a tipos fundamentalmente diferentes ( reglas de"alias estrictos" ). 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.

 47
Author: Jarod42,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-01-04 15:26:43

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, lo cual es curioso...

 10
Author: pepper_chico,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-01-04 16:13:20