- Foi incorporado à branch master do Zig um conjunto de mudanças que melhora o tratamento de inteiros não ABI no backend LLVM e introduz uma nova semântica para
@bitCast, resolvendo ao mesmo tempo problemas de otimização e inconsistências no comportamento da linguagem - Inteiros de largura de bits arbitrária como
u4,i13eu40passam a ser tratados como bit-int em valores SSA, mas são expandidos para inteiros de tamanho ABI ao serem armazenados em memória - O
@bitCastanterior era mais próximo de uma reinterpretação dos bytes em memória, mas a nova definição passa a se basear no arranjo lógico de bits do tipo, reduzindo a dependência de endian - A mudança foi estendida aos backends LLVM e C, além da execução em
comptime, e os usos relacionados na biblioteca padrão, no compilador e emcompiler_rttambém foram revisados - Com a recuperação de otimizações que estavam sendo perdidas, foi observada uma melhora de cerca de 5% de desempenho no próprio compilador Zig, e parte do código pode ver pequenos ganhos de performance em tempo de execução no 0.17.0
Mudança no tratamento de inteiros de largura de bits arbitrária no backend LLVM
- O Zig vinha fazendo lowering direto de tipos inteiros de largura de bits arbitrária como
u4,i13eu40para os tipos bit-int do LLVM IR, comoi4,i13ei40 - Essa abordagem fazia com que a semântica de representação em memória do LLVM impusesse restrições desnecessárias ao otimizador, e como o Clang não gera esse tipo de LLVM IR, esses caminhos internos do LLVM também não eram suficientemente testados
- Ao longo dos últimos anos, foram observados casos reais de otimizações perdidas e miscompilation
- A nova abordagem mantém tipos bit-int para manipulação de valores SSA, mas ao armazenar em memória faz zero-extend ou sign-extend para tipos de tamanho ABI como
i8,i16ei32 - Esse lowering se alinha à forma como o Clang faz lowering de
_BitInt(N)em C, o que deve aproveitar um caminho melhor suportado no LLVM
Limitações do @bitCast anterior
- O
@bitCastanterior era conceitualmente mais próximo do seguinte comportamento- obter o ponteiro para o valor do operando
- fazer cast desse ponteiro para um ponteiro do tipo de destino
- carregar o valor a partir desse ponteiro
- Em outras palavras, a definição anterior era mais próxima de uma reinterpretação de bytes em memória do que da estrutura lógica do tipo
- Com o tempo, o comportamento real se afastou dessa definição, e na maioria dos alvos era permitido fazer
@bitCastde[3]u8parau24mesmo quando@sizeOf(u24)é maior que@sizeOf([3]u8) - O backend LLVM implementava uma semântica de
@bitCastque não estava suficientemente especificada, e quando o modo de armazenar tipos inteiros em memória foi alterado, surgiram Illegal Behavior e crashes na suíte de testes do compilador - Em vez de adicionar ao backend LLVM uma lógica para imitar o comportamento anterior, foi escolhida a direção de implementar amplamente a nova definição de
@bitCast
Nova semântica de @bitCast
- A nova semântica se baseia na proposta de linguagem #19755, enviada e aceita em 2024
- Essa semântica já havia sido implementada no backend self-hosted x86_64, e nesta mudança foi estendida aos backends LLVM e C, além da execução em
comptime - O novo
@bitCastopera com base não nos bytes em memória, mas na ordem de bits que representa logicamente o tipou5é composto por 5 bits lógicos, do least-significant bit ao most-significant bit[2]u5é composto por 10 bits lógicos, em que os 5 bits do primeiro elemento são seguidos pelos 5 bits do segundo elemento
- Em conversões simples entre inteiros, como trocar
u8pori8do mesmo tamanho, os bits são preservados e o bit mais alto passa a ser interpretado como bit de sinal - A semântica de
@bitCastentre tipos inteiros epacked structoupacked uniontambém é mantida
Mudanças de comportamento em arrays e vetores
- O ponto em que a nova semântica difere da anterior é quando entram em jogo tipos agregados como arrays e vetores
- Por exemplo, ao fazer
@bitCastde[2]u8parau16, na semântica anterior o resultado variava conforme o endian do alvo- em alvos big-endian, o primeiro elemento do array se tornava os 8 bits mais altos
- em alvos little-endian, o primeiro elemento do array se tornava os 8 bits mais baixos
- A nova semântica considera apenas a representação lógica em bits, então é independente de endian, e em todos os alvos o primeiro elemento do array passa a ser os 8 bits mais baixos
- Em termos gerais, isso fica mais próximo do comportamento anterior em alvos little-endian
- Também passam a ser possíveis conversões menos convencionais, como transformar
[2]u3em@Vector(3, u2)- os bits lógicos do array são concatenados e então lidos em unidades de 2 bits para formar os elementos do vetor
- isso também pode ser usado para fazer
@bitCastde um inteiro para@Vector(n, u1)e assim decompor o valor em um vetor de bits individuais
Propostas incluídas junto e migração
- Durante esse trabalho, também foram implementadas pequenas propostas aceitas relacionadas a
@bitCast - Como a nova semântica difere de forma relevante da anterior, os usos de
@bitCastna biblioteca padrão, no compilador e em bibliotecas de suporte comocompiler_rtforam revisados - O PR relacionado é codeberg.org/ziglang/zig/pulls/35711, e com a fusão na master vários issues também foram fechados
- A semântica alterada e o procedimento de migração recomendado devem ser documentados nas notas de lançamento do Zig 0.17.0
Efeito esperado em desempenho no 0.17.0
- A mudança no lowering de inteiros não ABI no backend LLVM, que era o objetivo original, conseguiu recuperar otimizações que estavam sendo perdidas
- O resultado relacionado pode ser visto em demonstrably successful
- O próprio compilador Zig, embora internamente não use tantos inteiros de largura de bits arbitrária, ainda assim mostrou uma melhora de cerca de 5% de desempenho graças a otimizações melhores
- No 0.17.0, alguns códigos podem ter pequenos ganhos de performance em tempo de execução
1 comentários
Opiniões no Lobste.rs
O bit representation lógico mencionado no texto é dito como independente de endianness, mas a explicação real parece um método claramente little-endian que não oferece suporte à ordem de bits ou de bytes big-endian
Em um novo log de desenvolvimento datado de 25 de junho de 2026, foi informado que a nova semântica de
@bitCaste melhorias no backend LLVM foram mescladas em um pull request recenteÉ interessante, mas fico pensando se código escrito como abaixo não pode quebrar de repente em alvos big-endian raramente testados
Em pseudocódigo não-Zig:
Na prática, provavelmente não é um grande problema: entre os milhares de
@bitCastno repositório do Zig, parece que bem menos de 100 foram afetados por essa mudançaSinceramente, também não acho que a maioria dos usuários de Zig soubesse exatamente como
@bitCastfuncionava em conversões entre array/vetor e escalar. Muito código que antes era testado só no sistema do autor e funcionava apenas em little-endian agora provavelmente vai passar a funcionar em qualquer lugarComo ex-programador de C, lembro que os bit fields de C não eram muito populares porque seu comportamento não era portável entre arquiteturas
A nova semântica de
@bitCastem Zig parece exatamente a direção necessária por fornecer uma semântica abstrata portável que produz o mesmo resultado em arquiteturas diferentesEstou desenhando bit fields e bit casts na minha própria linguagem ultimamente, então pretendo olhar com mais atenção a documentação de design e implementação do Zig para esclarecer como meu código deve se comportar
packed structepacked union, e ambos são definidos de forma a combinar bem com a nova definição de@bitCastpacked structfunciona preenchendo os bits dos campos em um “inteiro base”. Por exemplo, se os campos forembool,u6,i9e o inteiro base foru16, então o bit menos significativo deu16será obool, os 6 bits seguintes serão ou6e os 9 bits restantes serão oi9. Ou seja, o packed struct de Zig é quase um açúcar sintático sobre vários shifts e máscaraspacked uniontambém tem um inteiro base, mas todos os campos precisam usar exatamente a mesma quantidade de bits que o inteiro base. Assim, gravar em um campo e ler por outro é quase idêntico ao@bitCastda nova semântica. Só que campos depacked union/packed structnão podem ter tipos array ou vetorPessoalmente, acho que essas ferramentas se encaixam bem para expressar “estruturas relacionadas a bits”. Dá para empacotar vários valores em um
packed structe usá-lo como os bit fields de C, e como é açúcar sintático sobre operações de bit, também permite expressar de forma limpa bit flags que em C costumavam virar um monte de macros sem segurança de tiposPor exemplo, flags de acesso RWX em C poderiam ser recebidas por uma API como macros
ACCESS_READ,ACCESS_WRITE,ACCESS_EXECjunto de umuint8_t, mas em Zig você pode definirAccess = packed struct(u8)com camposread,write,exec,reservede fazer a API receberAccessCom
packed structepacked union, também dá para representar layouts de bits bastante estranhos. A entrada da tabela de símbolos do formato de objeto Mach-O tem um campon_typepeculiar, aparentemente por razões históricas, e isso pode ser modelado comobits: packed struct(u8)estab: enum(u8)dentro de umapacked union(u8)Ao lidar com esse valor
n_type, não é preciso fazer shifts ou máscaras manualmente. Basta verificarn_type.bits.is_stab != 0e, se for verdadeiro, usarswitchemn_type.stab; caso contrário, olhar os outros campos den_type.bits. Também é possível construir valores como.{ .stab = .gsym }ou.{ .bits = .{ .ext = false, .type = .undf, .pext = false, .is_stab = 0 } }Já ficou um pouco longo e foge do tema do texto original por falar de outro recurso da linguagem, mas se você estiver procurando algo para consultar no design de uma nova linguagem, vale a pena experimentar
packed structepacked uniondo Zig. São ferramentas simples, mas muito boas na prática