(OU digitação de quibble, comportamento vago e alinhamento, oh meu Deus!)Amigos, resta muito pouco tempo para o lançamento de um novo tópico no curso
"Desenvolvedor C ++" . É hora de publicar uma tradução da segunda parte do material, que informa sobre o que um trocadilho está digitando.
O que é uma tipificação de trocadilhos?Chegamos ao ponto em que podemos nos perguntar por que precisamos de pseudônimos? Geralmente para a implementação de trocadilhos digitando, tk. métodos freqüentemente usados violam regras estritas de alias.

Às vezes, queremos contornar o sistema de tipos e interpretar o objeto como outro tipo. Reinterpretar um segmento de memória como outro tipo é chamado de
trocadilho de tipo. Os trocadilhos são úteis para tarefas que requerem acesso à representação básica de um objeto para visualizar, transportar ou manipular os dados fornecidos. Áreas típicas onde podemos encontrar o uso de trocadilhos: compiladores, serialização, código de rede, etc.
Tradicionalmente, isso era conseguido pegando o endereço do objeto, convertendo-o em um ponteiro para o tipo para o qual queremos interpretar e acessando o valor, ou seja, usando aliases. Por exemplo:
int x = 1 ; // C float *fp = (float*)&x ; // // C++ float *fp = reinterpret_cast<float*>(&x) ; // printf( “%f\n”, *fp ) ;
Como vimos anteriormente, esse alias é inaceitável, pois causará um comportamento indefinido. Mas, tradicionalmente, os compiladores não usavam regras estritas de alias, e esse tipo de código geralmente funcionava, e os desenvolvedores, infelizmente, estão acostumados a permitir tais coisas. Um método alternativo comum de digitação com trocadilhos é através da união, que é válida em C, mas causará um comportamento indefinido em C ++ (
veja o exemplo ):
union u1 { int n; float f; } ; union u1 u; uf = 1.0f; printf( "%d\n”, un ); // UB(undefined behaviour) C++ “n is not the active member”
Isso é inaceitável no C ++, e alguns acreditam que as uniões são destinadas apenas à implementação de tipos de variantes e consideram que o uso de uniões para digitar trocadilhos é um abuso.
Como implementar um trocadilho?O método padrão abençoado para digitar trocadilhos em C e C ++ é memcpy. Isso pode parecer um pouco complicado, mas o otimizador precisa reconhecer o uso do memcpy para o trocadilho, otimizá-lo e gerar um registro para registrar a movimentação. Por exemplo, se soubermos que int64_t tem o mesmo tamanho que o dobro:
static_assert( sizeof( double ) == sizeof( int64_t ) ); // C++17
Podemos usar o
memcpy
:
void func1( double d ) { std::int64_t n; std::memcpy(&n, &d, sizeof d); //…
Com um nível suficiente de otimização, qualquer compilador moderno decente gera código idêntico ao método reinterpret_cast mencionado anteriormente ou ao método join para obter um trocadilho. Estudando o código gerado, vemos que ele usa apenas o registro mov (
exemplo ).
Tipos e matrizes de trocadilhosMas e se quisermos implementar o trocadilho de uma matriz de caracteres não assinada em uma série de int não assinado e executar uma operação em cada valor int não assinado? Podemos usar o memcpy para transformar um array de caracteres não assinado em um tipo int temporário não cantado. O otimizador ainda poderá ver tudo através do memcpy e otimizar o objeto temporário e a cópia, além de trabalhar diretamente com os dados subjacentes (
exemplo ):
// , int foo( unsigned int x ) { return x ; } // , len sizeof(unsigned int) int bar( unsigned char *p, size_t len ) { int result = 0; for( size_t index = 0; index < len; index += sizeof(unsigned int) ) { unsigned int ui = 0; std::memcpy( &ui, &p[index], sizeof(unsigned int) ); result += foo( ui ) ; } return result; }
Neste exemplo, pegamos
char*p
, assumimos que ele aponta para vários fragmentos de
sizeof(unsigned int)
, interpretamos cada fragmento de dados como
unsigned int
, calculamos
foo()
para cada fragmento do trocadilho, somamos o resultado e retornamos o valor final .
A montagem para o corpo do loop mostra que o otimizador transforma o corpo em acesso direto à matriz de base de
unsigned char
como um
unsigned int
, adicionando-o diretamente ao
eax
:
add eax, dword ptr [rdi + rcx]
O mesmo código, mas usando
reinterpret_cast
para implementar um trocadilho (viola o aliasing estrito):
// , len sizeof(unsigned int) int bar( unsigned char *p, size_t len ) { int result = 0; for( size_t index = 0; index < len; index += sizeof(unsigned int) ) { unsigned int ui = *reinterpret_cast<unsigned int*>(&p[index]); result += foo( ui ); } return result; }
C ++ 20 e bit_castNo C ++ 20, temos o
bit_cast
, que fornece uma maneira fácil e segura de interpretar, e também pode ser usado no contexto do
constexpr
.
A seguir, é apresentado um exemplo de como usar
bit_cast
para interpretar um número inteiro não assinado em um
float
(
exemplo ):
std::cout << bit_cast<float>(0x447a0000) << "\n" ; //, sizeof(float) == sizeof(unsigned int)
No caso em que os tipos Para e De não tenham o mesmo tamanho, isso exige que usemos uma estrutura intermediária. Usaremos uma estrutura que contém uma matriz de caracteres múltiplos de
sizeof(unsigned int)
(um
sizeof(unsigned int)
4 bytes é assumido) como o tipo From e
unsigned int
como o To.
struct uint_chars { unsigned char arr[sizeof( unsigned int )] = {} ; // sizeof( unsigned int ) == 4 }; // len 4 int bar( unsigned char *p, size_t len ) { int result = 0; for( size_t index = 0; index < len; index += sizeof(unsigned int) ) { uint_chars f; std::memcpy( f.arr, &p[index], sizeof(unsigned int)); unsigned int result = bit_cast<unsigned int>(f); result += foo( result ); } return result ; }
Infelizmente, precisamos deste tipo intermediário - esta é a atual limitação
bit_cast
.
AlinhamentoNos exemplos anteriores, vimos que a violação de regras estritas de aliasing pode levar à exclusão de armazenamento durante a otimização. A violação de aliasing estrito também pode levar à violação dos requisitos de alinhamento. Tanto os padrões C quanto o C ++ dizem que os objetos estão sujeitos a requisitos de alinhamento que limitam o local onde os objetos podem ser colocados (na memória) e, portanto, acessíveis.
C11 seção 6.2.8 Alinhamento dos estados dos objetos :
Tipos completos de objetos têm requisitos de alinhamento que impõem restrições aos endereços nos quais objetos desse tipo podem ser colocados. Alinhamento é um valor inteiro definido pela implementação que representa o número de bytes entre endereços consecutivos nos quais esse objeto pode ser colocado. O tipo do objeto impõe um requisito de alinhamento em cada objeto desse tipo: um alinhamento mais rigoroso pode ser solicitado usando a
_Alignas
.
O padrão do projeto C ++ 17 na seção 1 [basic.align] :
Os tipos de objeto têm requisitos de alinhamento (6.7.1, 6.7.2) que impõem restrições aos endereços nos quais um objeto desse tipo pode ser colocado. Alinhamento é um valor inteiro definido pela implementação que representa o número de bytes entre endereços consecutivos nos quais um determinado objeto pode ser colocado. Um tipo de objeto impõe um requisito de alinhamento a todos os objetos desse tipo; Um alinhamento mais rigoroso pode ser solicitado usando o especificador de alinhamento (10.6.2).
C99 e C11 indicam explicitamente que uma conversão que resulta em um ponteiro não alinhado é um comportamento indefinido, seção 6.3.2.3.
Ponteiros diz:
Um ponteiro para um objeto ou tipo parcial pode ser convertido em um ponteiro para outro objeto ou tipo parcial. Se o ponteiro resultante não estiver alinhado corretamente para o tipo de ponteiro, o comportamento será indefinido. ...
Embora o C ++ não seja tão óbvio, acredito que esta frase do parágrafo 1
[basic.align]
suficiente:
... O tipo de objeto impõe um requisito de alinhamento em cada objeto desse tipo; ...
ExemploEntão, vamos supor:
- alignof (char) e alignof (int) são 1 e 4 respectivamente
- sizeof (int) é 4
Portanto, interpretar uma matriz de caracteres do tamanho 4 como
int
viola estritamente o alias e também pode violar os requisitos de alinhamento se a matriz tiver alinhamento de 1 ou 2 bytes.
char arr[4] = { 0x0F, 0x0, 0x0, 0x00 }; // 1 2 int x = *reinterpret_cast<int*>(arr); // Undefined behavior
O que pode resultar em desempenho reduzido ou erro de barramento em algumas situações. Enquanto o uso de alignas para forçar o mesmo alinhamento para uma matriz em int impedirá a quebra dos requisitos de alinhamento:
alignas(alignof(int)) char arr[4] = { 0x0F, 0x0, 0x0, 0x00 }; int x = *reinterpret_cast<int*>(arr);
AtomicidadeOutra punição inesperada por acesso desequilibrado é que ele viola a atomicidade de algumas arquiteturas. Os repositórios atômicos podem não parecer atômicos para outros threads no x86 se eles não estiverem alinhados.
Capturando violações estritas de aliasNão temos muitas boas ferramentas para rastrear aliasing estrito em C ++. As ferramentas que possuímos capturam alguns casos de violações e outros de carregamento e armazenamento inadequados.
O gcc usando os
-fstrict-aliasing
e
-Wstrict-aliasing
pode detectar alguns casos, embora não sem falsos positivos / problemas. Por exemplo, os seguintes casos gerarão um aviso no gcc (
exemplo ):
int a = 1; short j; float f = 1.f; // , TIS , printf("%i\n", j = *(reinterpret_cast<short*>(&a))); printf("%i\n", j = *(reinterpret_cast<int*>(&f)));
embora ele não entenda este caso adicional (
exemplo ):
int *p; p=&a; printf("%i\n", j = *(reinterpret_cast<short*>(p)));
Embora o
clang
resolva esses sinalizadores, ele não parece realmente implementar o aviso.
Outra ferramenta que temos é o ASan, que pode capturar gravação e armazenamento desalinhados. Embora não sejam violações diretas de aliasing estrito, esse é um resultado bastante comum. Por exemplo, os seguintes casos gerarão erros de tempo de execução durante a montagem usando clang usando
-fsanitize=address
int *x = new int[2]; // 8 : [0,7]. int *u = (int*)((char*)x + 6); // x *u = 1; // [6-9] printf( "%d\n", *u ); // [6-9]
A última ferramenta que eu recomendo é específica para C ++ e, de fato, não apenas uma ferramenta, mas também uma prática de codificação que não permite a conversão no estilo C. O
gcc
e o
clang
executam diagnósticos para as
-Wold-style-cast
C usando o
-Wold-style-cast
. Isso forçará todos os trocadilhos de digitação indefinidos a usar reinterpret_cast. Em geral,
reinterpret_cast
deve ser um farol para uma análise mais completa do código.
Também é mais fácil pesquisar na base de código
reinterpret_cast
para executar uma auditoria.
Para C, temos todas as ferramentas já descritas e também temos o
tis-interpreter
, um analisador estático que analisa exaustivamente o programa em busca de um grande subconjunto de C. Dadas as versões C do exemplo anterior, em que usar -fstrict-aliasing ignora um caso (
exemplo )
int a = 1; short j; float f = 1.0 ; printf("%i\n", j = *((short*)&a)); printf("%i\n", j = *((int*)&f)); int *p; p=&a; printf("%i\n", j = *((short*)p));
O intérprete TIS pode interceptar os três, o exemplo a seguir chama o kernel TIS como intérprete TIS (a saída é editada por questões de brevidade):
./bin/tis-kernel -sa example1.c ... example1.c:9:[sa] warning: The pointer (short *)(& a) has type short *. It violates strict aliasing rules by accessing a cell with effective type int. ... example1.c:10:[sa] warning: The pointer (int *)(& f) has type int *. It violates strict aliasing rules by accessing a cell with effective type float. Callstack: main ... example1.c:15:[sa] warning: The pointer (short *)p has type short *. It violates strict aliasing rules by accessing a cell with effective type int.
E, finalmente, o
TySan , que está em desenvolvimento. Este desinfetante adiciona informações de verificação de tipo ao segmento de memória de sombra e verifica os acessos para determinar se eles violam as regras de alias. A ferramenta deve ser capaz de rastrear todas as violações de alias, mas pode ter uma grande sobrecarga no tempo de execução.
ConclusãoAprendemos sobre regras de alias em C e C ++, o que significa que o compilador espera que sigamos rigorosamente essas regras e aceite as conseqüências de não cumpri-las. Aprendemos sobre algumas ferramentas que podem nos ajudar a identificar algum abuso de pseudônimo. Vimos que o uso usual de alias é um trocadilho de tipificação. Também aprendemos como implementá-lo corretamente.
Os otimizadores estão aprimorando gradualmente a análise de alias baseada em tipo e já quebrando algum código baseado em violações estritas de alias. Podemos esperar que as otimizações melhore e quebre ainda mais o código que funcionava antes.
Temos métodos compatíveis prontos para interpretação de tipos. Às vezes, para compilações de depuração, esses métodos devem ser abstrações livres. Temos várias ferramentas para detectar violações graves de aliasing, mas para C ++ elas capturam apenas uma pequena parte dos casos, e para C usando o tis-intérprete, podemos rastrear a maioria das violações.
Obrigado aos que comentaram este artigo: JF Bastien, Christopher Di Bella, Pascal Quoc, Matt P. Dziubinski, Patrice Roy e Olafur Vaage
Obviamente, no final, todos os erros pertencem ao autor.
Assim, a tradução de um material bastante grande chegou ao fim, cuja primeira parte pode ser lida
aqui . Tradicionalmente, convidamos você para
o dia da porta aberta , que será realizada em 14 de março pelo chefe do departamento de desenvolvimento de tecnologia da Rambler & Co -
Dmitry Shebordaev.