1 pontos por GN⁺ 2025-06-08 | 1 comentários | Compartilhar no WhatsApp
  • A otimização de baixo nível pode ser implementada com facilidade na linguagem Zig
  • O compilador realiza bem a otimização na maioria das situações, mas às vezes é preciso transmitir com clareza a intenção do programador para obter um desempenho melhor
  • Zig oferece geração de código de alto desempenho e metaprogramação poderosa com o recurso de execução em tempo de compilação (comptime)
  • Em comparação com Rust, Zig permite otimizações mais precisas por meio de anotações e de uma estrutura de código explícita
  • Em operações repetitivas como comparação de strings, é possível usar comptime para gerar código assembly superior ao de funções comuns

Otimização e Zig

Como diz o famoso alerta: "Tudo é possível, mas o que é interessante não se consegue com facilidade." A otimização de programas sempre foi uma preocupação central dos desenvolvedores. Para reduzir custos de infraestrutura em nuvem, melhorar latência e simplificar sistemas, a otimização de código é indispensável. Este texto explica os conceitos de otimização de baixo nível em Zig e destaca os pontos fortes da linguagem.

Podemos confiar no compilador?

  • Em geral, há muito o conselho de "confie no compilador", mas na prática há casos em que o compilador se comporta de forma diferente do esperado ou viola a especificação da linguagem
  • Linguagens de alto nível têm limitações de desempenho porque é difícil transmitir com clareza a intenção (intent)
  • Linguagens de baixo nível, por causa da explicitude do código, permitem que o compilador conheça as informações necessárias para otimização; por exemplo, ao comparar a função maxArray em JavaScript e em Zig, Zig informa em tempo de compilação — e não em tempo de execução — tipo, alinhamento, possibilidade de alias etc.
  • Ao escrever a mesma operação maxArray em Zig e Rust, obtém-se um código assembly de alto desempenho quase idêntico, mas quanto melhor a intenção for expressa, melhor tende a ser o resultado da otimização
  • Ainda assim, nem sempre dá para confiar no desempenho do compilador, então, em trechos críticos, é preciso verificar diretamente o código e o resultado da compilação e buscar formas de otimização

O papel do Zig

  • Zig, com sua explicitude precisa e suas ricas funções embutidas, ponteiros e anotações, comptime e um comportamento ilegal bem definido, consegue produzir código otimizado sem depender de informação abstrata
  • Em Rust, o modelo de memória garante por padrão a ausência de alias entre argumentos, mas em Zig é preciso usar anotações como noalias explicitamente
  • Se o critério for apenas LLVM IR, o nível de otimização do Zig também é elevado
  • Acima de tudo, o comptime (execução em tempo de compilação) do Zig é uma poderosa ferramenta de otimização

O que é comptime?

  • O comptime do Zig é usado para geração de código, incorporação de valores constantes e criação de estruturas genéricas baseadas em tipos, desempenhando papel importante na melhora do desempenho em tempo de execução
  • É possível implementar metaprogramação com comptime
  • Diferentemente de macros em C/C++ ou do sistema de macros de Rust, comptime não usa uma sintaxe separada: ele é código comum
  • O código de comptime não altera diretamente a AST e permite inspecionar, refletir e gerar elementos para todos os tipos em tempo de compilação
  • A flexibilidade de comptime influenciou melhorias em outras linguagens, como Rust, e está integrada ao Zig de forma natural

Limites de comptime

  • Alguns recursos de macro, como token-pasting, não podem ser substituídos pelo comptime do Zig
  • Como Zig valoriza a legibilidade do código, não permite criar variáveis fora do escopo nem definir macros desse tipo
  • Em compensação, o comptime do Zig oferece vários usos amplos de metaprogramação, como reflexão de tipos, implementação de DSLs e otimização de parsing de strings

Otimizando comparação de strings com comptime

  • Uma função comum de comparação de strings pode ser implementada em qualquer linguagem, mas em Zig, quando uma das duas strings é uma constante conhecida em comptime, é possível gerar código assembly mais eficiente
  • Por exemplo, se uma string for sempre "Hello!\n", é possível aplicar uma otimização que a compara em blocos maiores, e não byte a byte
  • Para isso, usando comptime, pode-se gerar em tempo de compilação código de alto desempenho com vetores SIMD, processamento em blocos e otimização dos bytes restantes
  • Com esse método, além de comparações repetitivas de strings, também é possível implementar código orientado a desempenho para diversos mapeamentos baseados em dados estáticos, tabelas de hash perfeitas, parsers de AST e muito mais

Conclusão

  • Zig é extremamente adequado para otimização de baixo nível e, graças à estrutura explícita do código e ao poderoso comptime, permite implementar diretamente o mais alto nível de desempenho
  • Mesmo em comparação com outras linguagens, como Rust, a capacidade de programação em tempo de compilação e a explicitude do Zig representam uma grande vantagem no desenvolvimento de software de alto desempenho
  • A capacidade de otimização do Zig tende a se tornar uma vantagem competitiva cada vez mais importante

1 comentários

 
GN⁺ 2025-06-08
Comentários no Hacker News
  • O que acho mais interessante em Zig é a simplicidade do sistema de build, a compilação cruzada e a busca por alta velocidade de iteração. Como sou desenvolvedor de jogos, desempenho é importante, mas para a maioria dos requisitos, a maioria das linguagens oferece desempenho suficiente. Então isso não é o principal critério na escolha de linguagem. Dá para escrever código robusto em qualquer linguagem, mas o objetivo é ter um framework voltado para o futuro que possa ser mantido por décadas. C/C++ acabou virando a escolha padrão por ser suportado em todo lugar, mas sinto que Zig também pode chegar lá
    • Por diversão, tentei rodar Zig em um Kindle muito antigo (Linux 4.1.15) e fiquei impressionado com o nível de maturidade do Zig. Quase tudo funcionou de cara, e até com um GDB antigo foi possível depurar bugs estranhos. Eu também fiquei fascinado por Zig. Dá para ver a experiência em mais detalhes aqui
    • Também sinto que dá para escrever código robusto na maioria das linguagens, mas quero código modular que possa atravessar décadas. Gosto de Zig, mas acho que ele tem desvantagens em manutenção de longo prazo e modularidade. Zig é uma linguagem hostil à encapsulação. Não é possível tornar privados os membros de uma struct. Este comentário na issue mostra um exemplo. A posição do Zig é que não deveria existir uma representação interna separada, e que todos os usuários devem conhecer a implementação interna por meio de documentação/divulgação pública. Mas, para preservar o contrato da API, que é o núcleo do software modular, você precisa poder esconder a implementação interna, e isso não é possível. Espero que algum dia Zig passe a oferecer suporte a campos privados
    • Usei Rust de forma superficial e gostei. Mas ouvi dizerem que era "ruim", então parei por um tempo e agora estou usando de novo. Ainda gosto dele. Não entendo muito bem por que tanta gente odeia. A sintaxe feia de genéricos também existe em C# e TypeScript. E o Borrow Checker é fácil de entender se você já tem experiência com linguagens de baixo nível
    • Zig parece um Rust mais simples e um Go melhor. Por outro lado, entre as ferramentas construídas sobre Zig, adoro bun a ponto de ficar impressionado. bun deixou minha vida muito mais prática. uv, feito em Rust, proporciona uma experiência parecida
    • Concordo que C/C++ são o padrão. Quase toda tentativa de criar algo melhor que C acabou virando C++ no fim. Mesmo assim, não devemos parar de tentar. Rust e Zig são a prova de que ainda dá para esperar algo melhor. Pretendo aprender mais C++ a partir de agora
  • Mesmo que compiladores de ponta às vezes quebrem a especificação da linguagem, a suposição do Clang de que loops infinitos terminam está correta segundo o padrão desde C11. Em C11, isso é especificado assim: "um loop cujo expressão de controle não é uma expressão constante e que não realiza operações de E/S/volatile/sync/atômicas pode ser assumido pelo compilador como terminando"
    • Em C++ (pelo menos até o futuro C++26), essa regra se aplica a todos os loops, mas, como você disse, em C ela só vale para "loops cuja expressão de controle não é uma expressão constante". Ou seja, um loop infinito explícito como for(;;); realmente deve ser infinito, e loop {} em Rust também deveria ser. Mas os desenvolvedores do LLVM às vezes agem como se estivessem fazendo apenas um compilador de C++, então mesmo quando Rust pede "por favor, faça um loop infinito", o LLVM aplica algo como "isso não acontece em C++ segundo a especificação, então vou otimizar!", o que causa problemas. Foi uma otimização errada aplicada à linguagem errada
  • Mesmo sem funcionalidade de comptime, fazer comparação de strings inline e desenrolada é algo perfeitamente possível em C. Exemplo relacionado
    • A observação está correta! O primeiro exemplo era simples demais. Um exemplo melhor é o autômato de sufixo em tempo de compilação. E, além disso, o código no godbolt linkado acima na verdade mostra um dos dois casos do que não se deve fazer
  • Acho que a parte que diz que o bytecode gerado no V8 para o exemplo em JavaScript é ineficiente não é uma boa comparação. Para Zig e Rust, foi exigido compilar mirando um ambiente bem moderno, enquanto no V8 esse tipo de opção de otimização não foi forçado. Na prática, JITs modernos também conseguem vetorizar quando as condições permitem. E a maioria das linguagens modernas trata otimizações de string de forma parecida. Como referência, há também um exemplo em C++
    • Comparar JS com Zig é, na prática, como comparar maçãs com salada de frutas. O exemplo em Zig usa arrays com tipo e tamanho fixos, enquanto em JS é código generic em que vários tipos podem aparecer em tempo de execução. Por isso, em JS, se você fornecer bem a informação de tipo, o JIT consegue gerar loops muito mais rápidos (ainda que talvez não chegue à vetorização). Na prática, TypedArray não é usado com tanta frequência porque o custo de inicialização é alto, e só vale a pena quando há muita reutilização. E o texto dizia que o código JS estava inflado, mas grande parte disso vem de guards inseridos porque o JIT não pode confiar na verificação do comprimento do array; na prática, todo mundo escreve loops como i < x.length, o que permite otimização do JIT. Nesse sentido é um pouco preciosismo, mas é uma diferença pequena
    • Dá para mudar os exemplos de Rust e Zig no godbolt para um CPU-alvo mais antigo também. Não tinha pensado na limitação de alvo do lado do JS. E o exemplo em C++ mostra como o clang gera código muito bom. Ainda assim, no estado atual, o assembly não é tão satisfatório assim (mesmo levando em conta que Zig é compilado para um CPU-alvo específico). Se existisse um exemplo de port do autômato de sufixo em tempo de compilação para C++, seria realmente interessante. Esse é um caso real de uso de comptime que um compilador C++ não consegue prever
  • Tenho dúvidas sobre a afirmação de que "linguagens de alto nível carecem da 'intent' que linguagens de baixo nível têm". Pelo contrário, vejo como vantagem das linguagens de alto nível justamente poder expressar intenção de formas mais diversas e detalhadas
    • Também concordo. No fundo, a diferença entre linguagens de alto nível e de baixo nível é que, nas de alto nível, você expressa intenção, e nas de baixo nível precisa expor o próprio mecanismo de implementação
    • Aqui, "intenção" não quer dizer algo de negócio como "calcular o imposto desta compra", mas algo mais próximo de dizer ao computador o que fazer, como "deslocar este byte três posições para a esquerda". Por exemplo, um código como purchase.calculate_tax().await.map_err(|e| TaxCalculationError { source: e })?; está cheio de intenção, mas é impossível prever como o código de máquina vai sair na prática
  • Gosto muito do modelo de allocator do Zig. Seria ótimo se no Go desse para usar algo como um allocator por requisição em vez de GC
    • Em Go, até dá para usar allocator customizado e arena, mas a usabilidade é muito ruim e é difícil usar isso de forma adequada. Também não há como expressar nem impor regras de ownership no nível da linguagem. No fim, vira quase um C com sintaxe um pouco diferente, e sem GC acaba sendo até mais perigoso que C++
  • Concordo com a ideia de que "gosto da verbosidade do Zig", mas, sinceramente, a expressão soa um pouco estranha. C é frouxo em vários pontos, enquanto Zig, no extremo oposto, muitas vezes exige ruído excessivo de anotações (annotation noise) — especialmente em casts explícitos de inteiros em expressões matemáticas. Veja este texto. Em termos de desempenho, quando Zig é mais rápido que C, muitas vezes é porque o Zig usa configurações mais agressivas de otimização do LLVM (-march=native, otimização de programa inteiro etc.). Na prática, em C também dá para usar dicas de otimização como unreachable por meio de extensões da linguagem, e o Clang também faz constant folding de forma bem agressiva. Ou seja, a diferença entre comptime em Zig e codegen em C muitas vezes vem das configurações de otimização do compilador. TL;DR: se C estiver lento, primeiro verifique a configuração do compilador. No fim das contas, o coração da otimização é o LLVM
    • No exemplo da parte de casts, na verdade dá até para criar uma função para encapsular o cast, aumentando a reutilização do código e deixando a intenção mais clara
      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);
      }
      
      Essa abordagem gera exatamente o mesmo assembly, é reutilizável e clara
    • As ideias do Zig são interessantes, e havia bem mais ênfase em comptime e compilação de programa inteiro do que eu esperava no artigo original. Concordo com isso. Como referência, Virgil já oferecia uso da linguagem inteira em tempo de compilação e suporte a compilação de programa inteiro desde 2006. Virgil não tem LLVM como alvo, então comparação de velocidade acaba sendo comparação de backend. Graças a essa abordagem, Virgil consegue fazer otimizações muito fortes, como devirtualizar chamadas de método antecipadamente, remover ao máximo campos/objetos não usados, propagar constantes até objetos de heap baseados em campos e especializar tudo de forma completa
    • Pensando no uso de IA no futuro, acho que linguagens cada vez mais explícitas e verbosas vão ganhar espaço. Independentemente de programar com IA ou de isso ser correto ou não, muitos desenvolvedores vão preferir a ajuda da IA, e as linguagens também vão mudar para se adaptar a isso
    • Se um novo backend x86 for introduzido, talvez no futuro vejamos casos em que a diferença de desempenho entre C e Zig venha do próprio projeto Zig
    • Quanto aos casts explícitos de integer, em breve deve sair uma melhoria para deixar isso mais limpo. Veja esta discussão
  • Fazer benchmark de uma linguagem em si, como em "C é mais rápido que Python", não faz muito sentido, mas alguns recursos da linguagem de fato se tornam grandes barreiras para otimização. Usando a linguagem apropriada, tanto o desenvolvedor quanto o compilador conseguem expressar a intenção de forma natural e rápida
  • Acho a sintaxe do for no Zig bagunçada demais. Ter que colocar duas listas lado a lado e alinhar suas posições já dói só de olhar. Acho um erro recente das linguagens despejar sintaxe "mágica" e símbolos especiais demais. Não parece algo para ficar olhando por horas
    • Esse padrão de percorrer dois arrays é muito comum em código de baixo nível, e iterar em paralelo também. Por isso, acho apropriado que Zig dê suporte claro e natural a isso. Fico curioso por que isso machuca tanto os olhos
  • Otimização é extremamente importante. O efeito dela só cresce com o tempo
    • Claro, isso vale sob a premissa de que o software realmente venha a ser usado