1 pontos por GN⁺ 2025-06-08 | 1 comentários | Compartilhar no WhatsApp
  • Otimização de baixo nível é o trabalho de fazer o compilador entender melhor as intenções e restrições do código, e o Zig se encaixa bem nesse objetivo por facilitar a explicitação de tipos, alinhamento, aliasing e informações de tempo de compilação
  • Mesmo compiladores otimizadores como o LLVM nem sempre geram o melhor código possível, então, em trechos com gargalo, ainda é necessário inspecionar o código gerado e ajustar o código-fonte
  • O Zig pode produzir código vetorizado menor do que o exemplo em JavaScript ao passar em tempo de compilação informações como noalias, align, tamanho fixo de array e tipo dos elementos
  • comptime executa código Zig comum em tempo de compilação, permitindo metaprogramação como geração de constantes, implementação de genéricos, reflexão de tipos e otimização de comparação de strings
  • A força do Zig está menos em macros que alteram diretamente a AST e mais na execução em tempo de compilação integrada à linguagem, além de permitir despachar alguns valores de runtime para funções especializadas em tempo de compilação

Por que confiar no compilador, mas ainda verificar

  • Otimização vai além da técnica de criar programas mais rápidos: ela também se conecta à redução de custos, ao ganho de escalabilidade e à manutenção da simplicidade do sistema
  • Compiladores modernos produzem resultados impressionantes com backends como o LLVM, mas em algumas situações ainda geram código subótimo
  • Linguagens de baixo nível são rápidas não só por terem menos overhead de garbage collection ou de interpretador, mas também porque conseguem expressar mais informações de intenção que o compilador pode entender
  • O compilador não consegue mudar o algoritmo nem o paradigma de programação em si, e em geral otimiza dentro de um escopo limitado, como loops

Exemplo de máximo entre arrays em JavaScript e Zig

  • O exemplo em JavaScript grava em x o maior valor entre os elementos de dois arrays, no formato x[i] = y[i] > x[i] ? y[i] : x[i]
  • Para humanos, o código é claro, mas o bytecode gerado pelo V8 é inflado
  • O exemplo em Zig declara com mais precisão, nos argumentos da função, as informações necessárias para otimização
    • noalias x: x não faz alias com outros ponteiros
    • *align(64): alinhamento de 64 bytes
    • [65536]f64: tamanho do array e tipo dos elementos
    • const: argumento somente leitura
  • Graças a essas informações, o compilador consegue gerar código melhor e, no exemplo, produz assembly vetorizado
  • Um código Rust equivalente também gera praticamente o mesmo assembly

Onde o Zig favorece a otimização e quais são seus limites

  • O Zig permite expressões verbosas para transmitir bastante informação sobre o código ao LLVM
  • Os principais elementos oferecidos pelo Zig em relação à otimização são:
  • O modelo de memória do Rust permite que o compilador assuma sempre que argumentos de função não criam alias, mas no Zig isso precisa ser indicado manualmente
  • Se o compilador não souber se os argumentos de uma função Zig têm aliasing, uma função Zig sem anotações pode ser mais lenta que uma função Rust
  • Mesmo tomando como base apenas LLVM IR bem anotado, o Zig entrega bons resultados, mas sua força maior está na execução em tempo de compilação

O papel de comptime

  • O comptime do Zig é um recurso para geração de código em tempo de compilação
  • Entre as tarefas possíveis em tempo de compilação estão:
    • gerar constantes e incluí-las no binário
    • evitar reescrever a mesma estrutura de hashmap para vários tipos de dados
    • induzir otimizações para remover código desnecessário com base em dados conhecidos em tempo de compilação
    • inspecionar, refletir e gerar tipos para implementar genéricos
  • Código comptime é código Zig comum executado em tempo de compilação e não pode ter efeitos colaterais como IO de rede
  • A máquina de emulação usada em tempo de compilação corresponde ao alvo de compilação
  • Quase todo código Zig pode ser executado em tempo de compilação com comptime, e todos os tipos podem ser inspecionados, refletidos e gerados nesse estágio

Como isso difere de macros

  • O objetivo de comptime é parecido com o de macros, mas o modo de funcionamento é diferente
  • Algumas macros alteram texto bruto, e outras modificam diretamente a AST do programa
  • O comptime do Zig não altera a AST diretamente, nem oferece recursos como macros de concatenação de tokens
  • Como o Zig busca ser uma linguagem legível, ele não combina com o estilo de macro que cria ou modifica variáveis em escopos não relacionados
  • Entre as tarefas que macros conseguem fazer, mas que o comptime do Zig não faz diretamente, estão:
    • definir outras macros
    • alterar a AST
    • implementar diretamente minilinguagens ou DSLs
  • Ainda assim, é possível criar DSLs em Zig, e a função print do Zig faz o parse da string de formatação com comptime para montar o grafo de funções que serializa os dados
  • Exemplos incluem TigerBeetle account testing DSL, comath, zilliam

Otimização de comparação de strings com comptime

  • Uma comparação de strings comum retorna false se os comprimentos forem diferentes e, se forem iguais, compara cada byte em sequência
  • Esse método exige ler e comparar bytes de ambas as strings
  • Muitas vezes uma das strings já é conhecida em tempo de compilação, então no Zig é possível exigir que um dos argumentos seja comptime
    • fn staticEql(comptime a: []const u8, b: []const u8) bool
  • Ao comparar com uma string estática como "Hello!\n", o compilador gera código composto por comparação de comprimento e comparações constantes de cada byte
  • O objetivo desta seção não é apenas mostrar otimizações que o compilador pode fazer automaticamente, mas mostrar que comptime pode forçar a transformação e abrir oportunidades que o compilador talvez não enxergue sozinho

Comparação em blocos maiores e uso de SIMD

  • A comparação simples de strings com comptime ainda faz comparações byte a byte
  • A versão melhorada usa std.simd.suggestVectorLength(u8) ou @sizeOf(usize) para definir o tamanho do bloco de comparação
  • Depois de verificar primeiro o comprimento da string, ela calcula quantos blocos grandes podem ser comparados e quantos bytes sobram
  • Cada bloco é convertido com @bitCast para um tipo inteiro criado por std.meta.Int(.unsigned, block_len * 8) e então comparado
  • Os bytes restantes também são comparados com um tipo inteiro separado
  • No exemplo com "Hello, World!\n", o assembly gerado usa registradores maiores e reduz o número de desvios condicionais
  • Em comparações de strings maiores, o assembly gerado passa a usar registradores SIMD maiores

Usando valores de runtime junto com especialização em tempo de compilação

  • O comptime do Zig não se limita apenas a dados conhecidos em tempo de compilação
  • Em casos simples, é possível gerar vários procedimentos em tempo de compilação e, depois, fazer despacho dinâmico para o procedimento adequado conforme o valor de runtime
  • O código de exemplo envia, no switch (runtime_val), valores do intervalo inline 0...100 para staticFn(comptime_val) e trata o restante com runtimeFn(runtime_val)
  • Se não se quiser aumentar o tamanho do binário, é possível fazer fallback para uma implementação totalmente em runtime

Conclusão

  • O comptime do Zig cumpre o papel de substituir templates, macros, genéricos e geração manual de código
  • É possível fazer coisas parecidas em outras linguagens, mas no Zig o comptime está integrado de forma mais natural à linguagem
  • O Zig facilita escrever código de alto desempenho em situações realmente úteis e contrasta com um Turing tar-pit, em que tudo é possível, mas qualquer tarefa interessante é difícil
  • Sobre guerras de linguagem, permanece tanto a visão ampla de que completude de Turing bastaria quanto a ideia de que as pessoas ainda podem ter linguagens preferidas
  • Frases como “C é mais rápido que Python”, que tratam a linguagem em si como objeto de benchmark, podem ser enganosas; na prática, o alvo do benchmark não é a linguagem, mas um código e uma implementação específicos

1 comentários

 
GN⁺ 2025-06-08
Opiniões no Hacker News
  • O que mais me atrai no Zig é que ele mira em facilidade do sistema de build, cross-compilation e ciclos rápidos de iteração
    Como desenvolvedor de jogos, tenho requisitos de desempenho, mas acho que a maioria das linguagens já entrega o desempenho necessário, então isso não é o critério nº 1 na escolha da linguagem
    É possível escrever código poderoso em qualquer linguagem, mas o ponto central é escolher um framework altamente preparado para o futuro que permita manter código modular por décadas
    C/C++ era a resposta padrão porque é suportado em todos os lugares, e parece que o Zig pode chegar a esse mesmo nível

    • Gosto de Zig, mas vejo manutenção de longo prazo e modularidade justamente como um dos seus pontos mais fracos
      Zig é hostil ao encapsulamento, e não é possível tornar membros de structs privados: https://github.com/ziglang/zig/issues/9909#issuecomment-9426...
      A citação central é a posição de que “campos privados e getters/setters são um antipadrão popularizado por Java; campos são dados que existem, então dê nomes a eles com cuidado e documente-os como parte da API pública”
      Sem conseguir ocultar a representação interna, fica difícil criar adequadamente o contrato de API que é a base da modularidade de software; deveria ser possível mudar a representação interna sem quebrar o código dos usuários
      A posição do Zig parece ser a de que não deveria existir algo separado chamado representação interna, e que a própria representação deve ser pública, documentada e garantida; espero que um dia revertam essa decisão e passem a oferecer suporte a campos privados
    • Por diversão, testei Zig em um Kindle antigo rodando um Linux 4.1.15 bastante enxuto, e foi uma experiência bem interessante; fiquei pleasantly surprised com a maturidade do Zig
      Muita coisa funcionou de primeira, e até consegui depurar bugs estranhos com um GDB antigo
      Eu também fui convencido pelo Zig, e escrevi sobre isso aqui: https://news.ycombinator.com/item?id=44211041
    • Usei um pouco de Rust e gostei, mas ouvi falar que tinha má reputação e parei por um tempo; quando voltei a usar, continuei gostando
      Não entendo muito bem por que o odeiam tanto
      Generics feios também existem em C# e TypeScript, e o borrow checker é um conceito compreensível se você já fez trabalho de baixo nível
    • Zig parece um Rust mais simples e um Go melhor
      Entre as ferramentas feitas sobre Zig, a que realmente me impressiona é o bun; depois que passei a usar bun, minha vida ficou muito mais simples
      Dá para dizer algo parecido sobre o uv, feito em Rust
    • Tenho curiosidade para saber como o Zig se sairia em consoles
      Em geral, consoles não gostam de nada que não seja C/C++, mas como Zig pode ser convertido para C, talvez não seja totalmente descartado
  • Sobre a afirmação de que “até compiladores modernos quebram a especificação da linguagem (o Clang assume que todos os loops sem efeitos colaterais terminam)”, não duvido que compiladores às vezes quebrem a especificação, mas, nesse caso, o Clang está correto pelo menos desde o C11
    O C11 diz que, se a expressão de controle não for uma expressão constante e o laço não realizar entrada/saída, acesso volatile, sincronização nem operações atômicas, a implementação pode assumir que ele termina

    • C++ diz isso sobre todos os loops até a chegada do C++26, mas, como apontado, C em si não diz isso; vale apenas para casos em que “a expressão de controle não é uma expressão constante”
      Portanto, em C, um loop infinito simples for (;;); deve de fato ser compilado como um loop infinito, e o loop {} menos opaco de Rust também deveria ser
      Só que o LLVM é feito por pessoas que às vezes esquecem que nem sempre estão criando um compilador C++; então houve um ponto em que, quando Rust dizia “me dê um loop infinito”, o LLVM respondia “segundo C++, isso não existe, então vou otimizar”, e isso foi uma aplicação incorreta a outra linguagem
  • Não é estritamente necessário usar comptime para inlinear e desdobrar comparações de strings
    Também é possível em C: https://godbolt.org/z/6edWbqnfT
    Corrigi o typo

  • Não acho que comparar o exemplo em JavaScript com os exemplos em Zig/Rust seja uma boa comparação
    Parece que mandaram os compiladores de Zig e Rust escolherem uma CPU-alvo muito moderna, enquanto o V8 não está nas mesmas condições
    Um JIT com otimização também sabe vetorizar quando as condições são adequadas
    Para referência, a maioria das linguagens modernas realiza a mesma otimização em strings, e há um exemplo em C++ aqui: https://godbolt.org/z/TM5qdbTqh

    • No geral, parece uma comparação entre maçãs e uma salada de frutas, mas é adequada para mostrar a diferença entre os usos de JS e Zig
      O exemplo em Zig usa arrays de tipo conhecido e tamanho fixo, enquanto o código JS é “genérico” em tempo de execução, de modo que x e y podem ser qualquer objeto
      Em JS, é correto pagar esse custo, mas, ironicamente, neste exemplo específico seria possível transmitir melhor as informações de tipo ao JIT
      Se você fizer essa função ser chamada sempre com Float64Array do mesmo tamanho, o JIT pode saber disso e gerar um loop mais rápido. Mesmo que não vetorize, fica muito melhor
      Só que arrays tipados têm um custo alto de inicialização, então, na prática, não são muito usados a menos que você aloque uma vez um array tipado grande e o reutilize muitas vezes
      O texto também diz que o bytecode JS ficou bem inflado; provavelmente uma parte grande disso vem dos guards inseridos porque o JIT não consegue garantir que 65536 seja igual ao tamanho dos dois arrays
      Ainda assim, na prática ninguém escreveria um loop for desse jeito, e sim i < x.length; nesse caso, o JIT remove pelo menos uma verificação de array
    • Nos exemplos de Rust e Zig no godbolt, dá para trocar o target para uma CPU mais antiga
      Desculpe por não ter pensado nas limitações do alvo JS
      O exemplo em C++ linkado é um bom exemplo do que o Clang consegue fazer em C++, mas, mesmo levando em conta que Zig compila para uma CPU específica, o assembly gerado parece um pouco decepcionante
      Seria muito interessante ver um port para C++ de https://github.com/RetroDev256/comptime_suffix_automaton
      Esse é um uso de comptime que um compilador C++ não consegue inferir de forma limpa
  • Não sei se a frase “linguagens de alto nível carecem da riqueza de intenção das linguagens de baixo nível” está realmente certa
    Expressão de intenção não parece ser um elemento do espectro alto nível/baixo nível; pelo contrário, quanto mais maneiras houver de expressar a intenção em detalhes, mais perto de alto nível deveria estar

    • Concordo e, indo além, acho que a diferença fundamental entre linguagens de alto nível e de baixo nível está no fato de que, nas de alto nível, expressamos intenção, enquanto, nas de baixo nível, somos obrigados a expressar o mecanismo subjacente
    • A intenção aqui parece estar mais para “desloque este byte três posições para a esquerda” do que para “calcule a alíquota deste pedido”
      É uma intenção sobre o que você quer fazer a máquina executar, mais do que sobre o que se quer alcançar
      Um código como purchase.calculate_tax().await.map_err(|e| TaxCalculationError { source: e })?; está cheio de intenção, mas não dá para saber qual código de máquina sairá no fim
  • Aquela sintaxe de loop for é horrível
    Quer dizer que há duas listas lado a lado e a posição de um item em uma lista corresponde à posição de um item na outra?
    Só de olhar já dói os olhos
    Parece que as linguagens modernas tomaram o caminho errado quando começaram a adicionar todo tipo de “mágica” ao parser e a espalhar pequenos símbolos por todo o código
    Não é um formato que eu queira encarar por horas

    • Arrays desse tipo são um padrão muito comum em código de baixo nível, independentemente da linguagem, e percorrê-los em paralelo também
      Por isso é natural que Zig ofereça uma sintaxe que facilite exatamente isso, deixando claro o que está acontecendo
      Pessoalmente acho que ficou muito bom; fico curioso para saber por que dói os olhos
  • Gosto muito do modelo de alocadores do Zig
    Seria bom se em Go também fosse possível usar algo como um alocador por requisição em vez de coleta de lixo

    • Em Go também são possíveis alocadores customizados e arenas, e eles existem de fato, mas a usabilidade é muito ruim e é difícil usá-los corretamente
      Como a própria linguagem não tem uma forma de expressar e impor regras de ownership, no fim é como escrever C com uma sintaxe um pouco diferente e torcer para dar certo
      Sem coleta de lixo, até C++ é muito mais seguro que Go
  • O modelo de memória do Rust permite que o compilador sempre assuma que argumentos de função nunca fazem referências com alias, mas, em Zig, isso precisa ser especificado manualmente
    Tenho evitado essas especificações de aliasing
    Poucas pessoas as entendem, e, se usadas incorretamente, podem introduzir bugs difíceis de compreender no código

  • Sobre a afirmação de que a flexibilidade do comptime do Zig trouxe boas melhorias para outras linguagens, execução de funções em tempo de compilação e funções que recebem argumentos constantes foram introduzidas por D em 2007 e levaram várias linguagens a adotar coisas semelhantes
    https://dlang.org/spec/function.html#interpretation

  • Gosto de Zig, mas dizer que “gosto da verbosidade do Zig” soa estranho
    É verdade que C é solto demais em vários cantos, mas, pelo padrão atual, Zig às vezes pende um pouco demais para o lado oposto e acaba gerando muito ruído de anotações de tipo que parecem comentários
    Isso acontece especialmente com casts inteiros explícitos em expressões; escrevi um pouco sobre isso aqui: https://floooh.github.io/2024/08/24/zig-and-emulators.html
    Em termos de desempenho, quando código Zig é mais rápido que código C semelhante, em geral é por causa das configurações de otimização do LLVM mais agressivas do Zig
    Por exemplo, por padrão o Zig usa -march=native e otimização de programa inteiro, e compila todo o código Zig do projeto como uma única unidade de compilação
    Quase todos os “truques”, como usar unreachable como dica de otimização, também são possíveis em C; só que às vezes exigem extensões de linguagem não padronizadas
    Compiladores C, especialmente o Clang, também fazem constant folding de forma muito agressiva e conseguem reduzir grandes regiões de código que podem ser dobradas para constantes, mesmo com pilhas de chamadas profundas
    Então, olhando apenas para a geração de código, muitas vezes não há grande diferença em relação ao comptime do Zig
    A vantagem do comptime é que ele não volta silenciosamente a ser código em tempo de execução, e código que não é comptime também fica sujeito às mesmas otimizações de constant folding que em C
    Por exemplo, se uma função “pura” não-comptime é chamada com argumentos constantes, o compilador ainda substitui a chamada da função pelo resultado
    Em resumo, se código C está mais lento que código Zig, é preciso verificar as configurações do compilador C. No fim, todo o trabalho pesado de otimização acontece por baixo, no LLVM

    • Quanto ao exemplo de casting, é possível encapsular o cast em uma função
      fn signExtendCast(comptime T: type, x: anytype) T { const ST = std.meta.Int(.signed, @bitSizeOf(T)); const SX = std.meta.Int(.signed, @bitSizeOf(@TypeOf(x))); return @bitCast(@as(ST, @as(SX, @bitCast(x)))); }
      export fn addi8(addr: u16, offset: u8) u16 { return addr +% signExtendCast(u16, offset); }
      Compila para o mesmo assembly, é reutilizável e deixa a intenção mais clara
    • Zig tem ideias interessantes, e eu esperava que o artigo se concentrasse mais em otimização de baixo nível, mas no fim o conteúdo era basicamente “comptime e compilação de programa inteiro são ótimos”
      Concordo com isso
      Virgil permite usar a linguagem inteira em tempo de compilação desde 2006 e também oferece suporte a compilação de programa inteiro
      Porém, como Virgil não tem LLVM como alvo, a comparação de velocidade acaba sendo uma comparação entre os backends dos dois compiladores
      Virgil se apoia bastante na análise de alcançabilidade e nas otimizações de especialização possibilitadas por esse modelo de compilação
      Por exemplo, ele desvirtualiza chamadas de método de forma agressiva, remove campos e objetos inalcançáveis, promove constantes por meio de campos e objetos no heap, e monomorfiza completamente código polimórfico
    • Quando o novo backend x86 sair, talvez seja possível ver que parte da diferença de desempenho entre C e Zig pode ser atribuída apenas ao próprio projeto Zig
    • Sobre casts inteiros explícitos, parece que em breve haverá uma arrumação nisso: https://ziggit.dev/t/short-math-notation-casting-clarity-of-...
    • Nos próximos anos, acho que o pêndulo geral das linguagens vai se mover com bastante força na direção de linguagens mais verbosas e explícitas
      Simplesmente porque isso as torna mais fáceis para a IA lidar
      Se usar IA para programar é uma boa ideia, e até que ponto, é uma questão à parte; mas muitos desenvolvedores acreditam nisso, e as linguagens tentarão acomodá-los