Por que a otimização elimina essa function?

Recentemente, tivemos uma palestra na universidade sobre programação de especialidades em vários idiomas.

O palestrante escreveu a seguinte function:

inline u64 Swap_64(u64 x) { u64 tmp; (*(u32*)&tmp) = Swap_32(*(((u32*)&x)+1)); (*(((u32*)&tmp)+1)) = Swap_32(*(u32*) &x); return tmp; } 

Embora eu entenda totalmente que esse também é um estilo muito ruim em termos de legibilidade, seu ponto principal era que essa parte do código funcionava bem no código de produção até que eles permitissem um alto nível de otimização. Então, o código não faria nada.

Ele disse que todas as atribuições para a variável tmp seriam otimizadas pelo compilador. Mas por que isso aconteceria?

Eu entendo que há circunstâncias em que as variables ​​precisam ser declaradas voláteis, de modo que o compilador não as toque, mesmo se ele achar que elas nunca são lidas ou escritas, mas eu não saberia por que isso aconteceria aqui.

Esse código viola as regras de aliasing restritas, o que torna ilegal acessar um object por meio de um ponteiro de um tipo diferente, embora o access por meio de * char ** seja permitido. O compilador tem permissão para assumir que os pointers de diferentes tipos não apontam para a mesma memory e otimizam adequadamente. Isso também significa que o código invoca um comportamento indefinido e poderia realmente fazer qualquer coisa.

Uma das melhores referências para este tópico é Understanding Strict Aliasing e podemos ver que o primeiro exemplo é similar ao código do 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); } 

O artigo explica que esse código viola as regras de aliasing restrito, já que sp é um alias de arg mas tem tipos diferentes e diz que, embora compile, é provável que arg permaneça inalterado depois que swap_words retornar. Embora com testes simples, não consigo reproduzir esse resultado com o código acima nem com o código de OPs, mas isso não significa nada, já que esse comportamento é indefinido e, portanto, não é previsível.

O artigo continua falando sobre muitos casos diferentes e apresenta várias soluções de trabalho, incluindo tipagem por meio de uma união, que é bem definida em C99 1 e pode ser indefinida em C ++, mas na prática é suportada pela maioria dos principais compiladores, por exemplo aqui é a referência do gcc na punção de tipos . O tópico anterior Propósito das Uniões em C e C ++ entra nos detalhes. Embora existam muitos tópicos sobre este tópico, isso parece fazer o melhor trabalho.

O código para essa solução é o seguinte:

 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 referência, a seção relevante do esboço da norma C99 sobre o aliasing estrito é 6.5 Expressions, parágrafo 7, que diz:

Um object deve ter seu valor armazenado acessado apenas por uma expressão lvalue que tenha um dos seguintes tipos: 76)

– um tipo compatível com o tipo efetivo do object,

– uma versão qualificada de um tipo compatível com o tipo efetivo do object,

– um tipo que é o tipo assinado ou não assinado correspondente ao tipo efetivo do object,

– um tipo que é assinado ou não assinado correspondente a uma versão qualificada do tipo efetivo do object,

– um tipo agregado ou sindical que inclua um dos tipos mencionados entre seus membros (incluindo, recursivamente, um membro de um sindicato subagregado ou contido), ou

– um tipo de personagem.

e a nota de rodapé 76 diz:

A intenção desta lista é especificar as circunstâncias nas quais um object pode ou não ser aliado.

e a seção relevante do padrão de esboço C ++ é 3.10 Lvalues ​​and rvalues paragraph 10

O artigo Type-punning e strict-aliasing dá uma introdução mais suave mas menos completa ao tópico e o C99 revisitado fornece uma análise profunda de C99 e aliasing e não é leitura de luz. Esta resposta para Acessando membro inativo do sindicato – indefinido? repassa os detalhes enlameados da tipagem através de uma união em C ++ e também não é leitura leve.


Notas de rodapé:

  1. Citando o comentário de Pascal Cuoq: […] C99 que foi inicialmente mal formulado, parecendo tornar o uso de punção por meio de sindicatos indefinido. Na realidade, o tipo de punição por sindicatos é legal em C89, legal em C11, e foi legal em C99 o tempo todo, embora tenha sido necessário até 2004 para o comitê corrigir a redação incorreta e a versão subsequente do TC3. open-std.org/jtc1/sc22/wg14/www/docs/dr_283.htm

Em C ++, argumentos de ponteiro são assumidos como não alias (exceto char* ) se eles apontarem para tipos fundamentalmente diferentes ( regras de “estrito aliasing” ). Isso permite algumas otimizações.

Aqui, o u64 tmp nunca é modificado como u64 .
Um conteúdo de u32* é modificado, mas pode não estar relacionado a ‘ u64 tmp ‘, portanto, pode ser visto como u64 tmp para u64 tmp .

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); ^ 

O Clang 3.4 não avisa em nenhum nível de otimização, o que é curioso …