1 pontos por GN⁺ 5 시간 전 | 1 comentários | Compartilhar no WhatsApp
  • 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, i13 e u40 passam 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 @bitCast anterior 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 em compiler_rt també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, i13 e u40 para os tipos bit-int do LLVM IR, como i4, i13 e i40
  • 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, i16 e i32
  • 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 @bitCast anterior 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 @bitCast de [3]u8 para u24 mesmo quando @sizeOf(u24) é maior que @sizeOf([3]u8)
  • O backend LLVM implementava uma semântica de @bitCast que 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 @bitCast opera com base não nos bytes em memória, mas na ordem de bits que representa logicamente o tipo
    • u5 é 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 u8 por i8 do mesmo tamanho, os bits são preservados e o bit mais alto passa a ser interpretado como bit de sinal
  • A semântica de @bitCast entre tipos inteiros e packed struct ou packed union també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 @bitCast de [2]u8 para u16, 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]u3 em @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 @bitCast de 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
    • proibição de @bitCast com vetores de ponteiros: #18936
    • permissão de @bitCast para enum: parte de #35602
  • Como a nova semântica difere de forma relevante da anterior, os usos de @bitCast na biblioteca padrão, no compilador e em bibliotecas de suporte como compiler_rt foram 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

 
GN⁺ 5 시간 전
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

    • Aqui, independente de endianness aparentemente significa que o comportamento não muda entre arquiteturas little-endian e big-endian
  • Em um novo log de desenvolvimento datado de 25 de junho de 2026, foi informado que a nova semântica de @bitCast e 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:

    if target_is_little_endian {  
        my_int = @bitCast(my_array);  
    } else {  
        my_int = @bitCast([my_array[1], my_array[0]]);  
    }  
    
    • Também pensei nisso, mas no fim acho que adiar uma mudança inevitável só faria o problema crescer
      Na prática, provavelmente não é um grande problema: entre os milhares de @bitCast no repositório do Zig, parece que bem menos de 100 foram afetados por essa mudança
      Sinceramente, também não acho que a maioria dos usuários de Zig soubesse exatamente como @bitCast funcionava 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 lugar
  • Como 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 @bitCast em Zig parece exatamente a direção necessária por fornecer uma semântica abstrata portável que produz o mesmo resultado em arquiteturas diferentes
    Estou 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

    • A principal alternativa do Zig aos bit fields de C provavelmente é packed struct e packed union, e ambos são definidos de forma a combinar bem com a nova definição de @bitCast
      packed struct funciona preenchendo os bits dos campos em um “inteiro base”. Por exemplo, se os campos forem bool, u6, i9 e o inteiro base for u16, então o bit menos significativo de u16 será o bool, os 6 bits seguintes serão o u6 e os 9 bits restantes serão o i9. Ou seja, o packed struct de Zig é quase um açúcar sintático sobre vários shifts e máscaras
      packed union també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 @bitCast da nova semântica. Só que campos de packed union/packed struct não podem ter tipos array ou vetor
      Pessoalmente, acho que essas ferramentas se encaixam bem para expressar “estruturas relacionadas a bits”. Dá para empacotar vários valores em um packed struct e 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 tipos
      Por exemplo, flags de acesso RWX em C poderiam ser recebidas por uma API como macros ACCESS_READ, ACCESS_WRITE, ACCESS_EXEC junto de um uint8_t, mas em Zig você pode definir Access = packed struct(u8) com campos read, write, exec, reserved e fazer a API receber Access
      Com packed struct e packed 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 campo n_type peculiar, aparentemente por razões históricas, e isso pode ser modelado como bits: packed struct(u8) e stab: enum(u8) dentro de uma packed union(u8)
      Ao lidar com esse valor n_type, não é preciso fazer shifts ou máscaras manualmente. Basta verificar n_type.bits.is_stab != 0 e, se for verdadeiro, usar switch em n_type.stab; caso contrário, olhar os outros campos de n_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 struct e packed union do Zig. São ferramentas simples, mas muito boas na prática