O andamento da reescrita do compilador Roc de Rust para Zig
(rtfeldman.com)- Para resolver falhas estruturais da implementação existente, 300 mil linhas em Rust foram reescritas em Zig; após 487 dias, chegou-se à paridade funcional, com a meta de lançar ainda neste ano a primeira versão numerada oficial, a 0.1.0
- O novo compilador oferece suporte a hot code loading e cross-compilation reproduzível, interpolação de strings dentro de pattern matching e remoção de alocações no heap no roteamento HTTP; também reduziu o tamanho em wasm do
Rocci Birdpara 31 KB, menos da metade do anterior - Os principais motivos para escolher Zig foram tempo de build, alocadores granulares e controle de layout de dados, um ecossistema adequado ao desenvolvimento de compiladores e verificação de código sem segurança de memória; a build incremental do Zig 0.17.0 recompila cerca de 460 mil linhas em 35 ms
- Na classificação de bugs reais, o compilador em Rust teve 21 casos de corrupção de memória e o compilador em Zig teve 10, mas a maioria se devia a geração incorreta de código; os erros de segurança de memória no próprio compilador Zig foram 2 casos de use-after-free que corrompiam nomes de arquivos
- Zig se encaixou bem em estruturas de dados sem ponteiros, desserialização sem parsing e reutilização do serializador de LLVM bitcode, mas a experiência de desenvolvimento em Rust foi melhor em liberação automática de memória em testes, polimorfismo, campos privados de structs, detecção de código morto e compatibilidade entre releases
Reescrita que chegou à paridade funcional
- A equipe do compilador Roc reescreveu cerca de 300 mil linhas de Rust em Zig ao longo de aproximadamente um ano e meio, chegando à paridade funcional com o compilador existente
- A paridade funcional é um marco importante, mas não é um lançamento oficial; a versão 0.1.0 do novo compilador está prevista para o fim deste ano
- Tornou-se possível compilar o jogo de WASM-4 de 2024 Rocci Bird com o novo compilador
- O jogo inteiro tem menos de 1.000 linhas de código Roc
- O código-fonte atualizado é mais conciso que o original
- O binário wasm gerado por
roc build --opt=sizetem 31 KB, menos da metade do resultado do compilador anterior
- No site, um compilador WebAssembly de 2,5 MB permite escrever e executar no navegador um programa Roc básico
- A reescrita levou 487 dias, 476 dias a mais que os 11 dias que a Bun levou para migrar cerca de 500 mil linhas de Zig para Rust
- A Bun fez um porte direto, enquanto a Roc esteve mais próxima de uma reescrita que alterou significativamente várias estruturas e funcionalidades do compilador
- Portanto, é difícil comparar diretamente o tamanho ou a duração dos dois codebases apenas em termos de Rust e Zig
Experiência de desenvolvimento oferecida pelo novo compilador
-
Hot code loading e cross-compilation
- Depois de executar um servidor com
roc server.roc, ao modificar o código, o novo código é aplicado automaticamente a partir da próxima requisição - É um comportamento comum em linguagens interpretadas como Python, mas pouco comum em linguagens compiladas de alto desempenho como Roc
- Funciona da mesma forma não só em servidores web, mas também em jogos 2D simples
- Na hora de distribuir,
roc build server.rocgera um binário standalone com otimizações do LLVM aplicadas - Com
roc build --target=x64musl, é possível fazer cross-compilation de um binário estático para Alpine Linux - Ao receber os mesmos bytes de código-fonte como entrada, qualquer sistema de build, inclusive Mac, gera os mesmos bytes de saída
- Depois de executar um servidor com
-
Interpolação de strings dentro de pattern matching
- Interpolações de string como
"/users/${id}"são suportadas dentro de pattern matching - Em vez de fazer parsing de strings de template em tempo de execução, como no roteamento ao estilo Express, o compilador trata isso diretamente
- É possível fazer matching simultâneo do método HTTP e do caminho, e ramificar por padrões também em caminhos aninhados ou valores padrão
- O código completo de tratamento de requisições HTTP do exemplo verifica a segurança de tipos em tempo de compilação e não realiza nenhuma alocação no heap
- Usando execução em tempo de compilação de funções puras, o roteamento de requisições HTTP foi implementado sem alocação, e a sintaxe pode ser testada no compilador WebAssembly do site
- Interpolações de string como
Por que reescrever do zero
- Roc não é uma linguagem de sistemas, ao contrário de Rust, C e Zig, e usa gerenciamento automático de memória baseado em contagem de referências
- Evita pausas de garbage collectors com rastreamento
- Aproveita a otimização Perceus e alterações oportunistas no estilo Koka
- Ao contrário de linguagens não sistêmicas comuns, não aloca heap a cada captura de closure; em vez disso, usa defuncionalização polimórfica por especialização de conjuntos de lambdas
- A defuncionalização, em linguagens funcionais, permite várias otimizações posteriores, como inlining, mas tornar a implementação existente correta era extremamente difícil
- O protótipo em OCaml de Ayaz Hafiz confirmou que o problema era uma falha estrutural espalhada por várias etapas do compilador, e que resolvê-lo exigiria reescrever a maior parte do compilador
- Outros contribuidores também planejavam reescrever diversas partes por motivos diferentes, então a equipe passou a considerar uma reescrita completa em vez de uma abordagem do navio de Teseu, substituindo quase tudo gradualmente
- Em projetos de compiladores bem-sucedidos, é comum haver reescritas do zero, por exemplo para self-hosting, e há também o caso da reescrita do TypeScript para Go, que não é a linguagem do próprio TypeScript
- Embora a política seja que o compilador Roc não faça self-hosting, desta vez a equipe concluiu que os benefícios da reescrita superavam os custos conhecidos
Critérios para escolher Zig em vez de Rust novamente
- A equipe já usava Zig em várias funcionalidades básicas da biblioteca padrão e, como as únicas linguagens de sistemas que conhecia bem o suficiente eram Rust e Zig, avaliou seriamente apenas essas duas
- Cada projeto tem uma linguagem adequada diferente, e continuar usando Rust em outros trabalhos não contradiz a escolha de Zig para Roc
-
Tempo de build
- As builds com
cargoem Rust demoravam muito, inclusive builds incrementais, e continuaram sendo um grande incômodo à medida que o codebase crescia - A expectativa era que a migração para Zig tornasse as builds muito mais rápidas
- As builds com
-
Controle de memória e layout de dados
- Cada etapa de compilação usa vários alocadores, especialmente alocadores de arena, e o layout struct of arrays (SoA) é amplamente utilizado
- O ecossistema Rust em geral pressupõe um único alocador global, enquanto o ecossistema Zig adota por padrão a passagem de alocadores granulares e também oferece suporte a SoA na biblioteca padrão
-
Escopo do ecossistema necessário
- O ecossistema Rust como um todo é maior que o de Zig, mas em ambos havia poucos pacotes relacionados às necessidades específicas da Roc
- Funcionalidades de nicho necessárias, como código que gera bitcode rapidamente em vez de envolver as bibliotecas C++ do LLVM, eram mais numerosas no lado de Zig
-
Suporte a código sem segurança de memória
- As 300 mil linhas do compilador Rust existente tinham cerca de 1.200 usos de
unsafe - Como comparação, o próprio compilador Rust usa cerca de 40 mil
unsafeem 3,5 milhões de linhas - Em um compilador que gera código de máquina, operações sem segurança de memória são uma parte importante do trabalho
- Rust segue um modelo de isolar blocos
unsaferaros e verificá-los com Miri ou Valgrind, mas em Roc ounsafenão era raro - Zig oferecia mais recursos para fazer código sem segurança de memória funcionar corretamente, e era exatamente nessa área que a equipe queria mais apoio
- As 300 mil linhas do compilador Rust existente tinham cerca de 1.200 usos de
Segurança de memória obtida sem borrow checker
-
Escopo das verificações em Rust e Zig
- Segundo um material da Microsoft de 2019, cerca de 70% das vulnerabilidades tratadas a cada ano por atualizações de segurança eram problemas de segurança de memória
- Dividindo a classificação de 2018 desse material sob a perspectiva de Rust e Zig, o quadro fica assim
- 83,6% eram leituras/escritas fora dos limites, casts inseguros, leituras não inicializadas, stack overflow e problemas que não eram de segurança de memória — categorias que não são afetadas pela escolha entre Rust e Zig
- 16,4% eram use-after-free, uma categoria que pode ser detectada pelo borrow checker do Rust, pelo Zig
ReleaseSafee por verificações no estilo Fil-C - O
ReleaseSafedo Zig causa um pânico em tempo de execução quando memória já liberada é usada- A verificação é menos abrangente do que o subconjunto seguro do Rust
- Há custo de execução e o programa pode entrar em pânico
- O
ReleaseFastomite as verificações em produção, mas as mantém em builds de debug e testes - Se todos os caminhos reais de execução forem testados, é possível obter uma segurança semelhante à de
ReleaseSafe, mas essa cobertura de testes geralmente não é realista
-
Outros projetos em Zig e o
unsafedo Rust- O TigerBeetle usa
ReleaseSafee, em uma rigorosa verificação com Jepsen, foram encontrados 2 bugs de segurança, mas nenhum deles tinha relação com segurança de memória - Ghostty e o compilador Zig, que usam
ReleaseFast, também não têm CVEs causados por falta de segurança de memória no código Zig - Programas Rust também podem ter lacunas de segurança de memória por meio de
unsafedentro de dependências - Unsafe Rust tem riscos semelhantes aos do Zig em
ReleaseFast, mas não há um recurso equivalente às verificações em tempo de execução do Zig que detectam problemas durante o desenvolvimento - Miri e Valgrind podem ajudar, mas não há muitos projetos Rust que os utilizem
- Em vez disso, a cultura de usar
unsaferaramente e revisá-lo com mais rigor é o que, na prática, constrói a forte reputação do Rust em segurança de memória - Também houve casos de vulnerabilidades relacionadas a
unsafeem projetos baseados em Rust- O Deno teve CVEs de leitura fora dos limites e use-after-free
- O Rocket teve um CVE de use-after-free
- O Actix teve vários CVEs de falta de segurança de memória no período em que o uso de
unsafeera anormalmente alto
- Como, no Roc, a maior parte das alocações ocorre em arenas com tempos de vida simples, use-after-free não foi visto como um grande risco, e avaliou-se que as verificações adicionais do Zig seriam mais úteis para código inerentemente inseguro
- O TigerBeetle usa
Bugs de corrupção de memória confirmados após a reescrita
- O resultado da classificação do issue tracker do Roc com Claude Opus 4.8 foi o seguinte
| Compilador | Houve corrupção de memória | Sem corrupção de memória | Total |
|---|---|---|---|
| Rust | 21 | 2.575 | 2.596 |
| Zig | 10 | 421 | 431 |
- Os 21 casos de corrupção de memória no compilador Rust não eram corrupção da lógica interna do compilador
- O borrow checker cumpriu o papel pretendido
- Eram bugs de compilação incorreta em que o código de máquina gerado erroneamente corrompia a memória no programa compilado
- Dos 10 casos de corrupção de memória no compilador Zig, 8 também eram compilações incorretas
- Os 2 restantes eram use-after-free no código de relatório de erros
- Nas mensagens de erro de
roc checkeroc bundle, o nome do arquivo aparecia corrompido como o caractere de substituição U+FFFD - O borrow checker do Rust teria conseguido detectar ambos os bugs
- Nas mensagens de erro de
- O resultado que uma escolha diferente de ferramenta teria causado para usuários reais é o seguinte
| Escolha de ferramenta | Impacto real na segurança de memória |
|---|---|
Zig ReleaseFast |
2 bugs em que o nome do arquivo não era renderizado em algumas mensagens de erro |
Zig ReleaseSafe |
2 bugs em que alguns erros causavam pânico e não conseguiam renderizar o nome do arquivo |
| Rust borrow checker | Prevenção de ambos os bugs |
- Considerando 18 meses, centenas de milhares de linhas e centenas de relatórios de bugs, a diferença entre as três opções não foi substancial para o projeto
- O Bun lida ao mesmo tempo com valores JavaScript de garbage collection por rastreamento e valores gerenciados manualmente, de modo que use-after-free, double-free e vazamentos por falta de liberação tiveram grande peso
- O compilador Roc não interage com JavaScript nem com outro garbage collector por rastreamento, portanto não sofre dos mesmos problemas de gerenciamento de tempo de vida
- O Roc precisa mais de ferramentas para encontrar erros de memória no código de saída gerado do que erros internos de memória no compilador, e estes últimos estão fora do escopo do borrow checker
Resultados reais de tempo de build
zig build --watch -fincrementalatualmente recompila alterações em cerca de 450 mil a 460 mil linhas de código Zig em aproximadamente 35 ms- A versão estável Zig 0.16.0 tem um bug que quebra
-fincrementalna base de código do Roc- A correção já foi incorporada, mas para usá-la é preciso migrar para uma pré-build do Zig 0.17.0 com quebra de compatibilidade
- Como também seria necessário vendorizar as dependências relacionadas e atualizá-las para 0.17.0, decidiu-se aguardar a próxima versão estável
- Os resultados medidos em um desktop Intel com Ubuntu 26 foram os seguintes
| Compilador Roc | Tamanho do código | Build frio | Build incremental |
|---|---|---|---|
| Original baseado em Rust 1.85.0 | 354K | 32,4 s | 10,0 s |
| Original baseado em Rust 1.97.0 | 354K | 25,4 s | 3,4 s |
| Zig 0.16.0 no ponto de paridade funcional | 320K | 39,6 s | 8,6 s |
| Reescrita atual em Zig 0.17.0 | 464K | 32,1 s | 0,035 s |
- No ponto de paridade funcional, até artefatos que quase não mudavam eram reconstruídos sempre, mas agora eles só são gerados quando necessário
- Com essa mudança, mesmo com o código cerca de 50% maior, o build frio ficou mais rápido
- Ao passar de Rust 1.85.0 para 1.97.0, o build incremental caiu de 10 s para 3,4 s, uma melhora de cerca de dois terços ao longo de 18 meses
- Os 35 ms do Zig equivalem a cerca de 1/100 de 3,4 s, e esse resultado vem de uma base de código cerca de 50% maior do que a usada na medição em Rust
- Atualmente,
-fincrementalfunciona apenas em x86-64, e muitos contribuidores usam Macs baseados em ARM, portanto ainda não é possível aproveitar totalmente a vantagem no tempo de build
Estruturas sem ponteiros e desserialização sem parsing
- O novo cache em disco usa como formato em disco o mesmo layout de memória eficiente para execução, de modo semelhante ao que é usado no compilador Zig e no desenvolvimento de jogos
- Todas as estruturas de dados do compilador são representadas por arrays de índices de 32 bits em vez de ponteiros, e em vários pontos usam o formato de arrays de structs
- Reduz o uso de memória e aumenta a velocidade de execução
- Permite gravar as estruturas de dados diretamente no disco, sem convertê-las para um formato de serialização separado
- Ao desserializar, os bytes do disco não são analisados por parsing
- Lê os bytes para a memória
- Faz alguns realocamentos para que as estruturas de dados existentes apontem para os novos arrays
- Depois disso, elas podem ser usadas imediatamente
- A velocidade fica, na prática, limitada pela velocidade de I/O de leitura de bytes do disco para a memória
- Se os dados estiverem no cache de disco do sistema operacional, os resultados do build anterior são carregados aproximadamente na velocidade de
memcpy
- Se os dados estiverem no cache de disco do sistema operacional, os resultados do build anterior são carregados aproximadamente na velocidade de
roc checksalva no disco, na primeira execução, os resultados de parsing, verificação de tipos etc.- Se o código-fonte de entrada não mudou, na segunda execução as estruturas de dados são movidas diretamente do disco para a memória
roc testtambém armazena em cache os resultados de testes determinísticos de funções puras- O cache funciona por arquivo; portanto, ao alterar um arquivo, apenas esse arquivo e seus arquivos dependentes são processados novamente
- Essa abordagem é possível porque o compilador inteiro usa índices em vez de ponteiros; em estruturas comuns centradas em ponteiros, a desserialização sem parsing é impossível
-
Limites de segurança de estruturas baseadas em índices
- Assim como um ponteiro pode apontar para um endereço incorreto, um índice também pode consultar o array errado e ler bytes arbitrários
- O borrow checker do Rust trata do tempo de vida de ponteiros, mas não verifica a qual array um determinado índice pertence
- Se a quantidade necessária de arrays for conhecida de antemão, o
compact_arenado Rust pode criar tags de tipo por macros para impedir consultas ao array errado - Quando a quantidade de arrays varia conforme o número de módulos, como no Roc, essa técnica não pode ser aplicada; por isso o
compact_arenatambém marcaSmallArena::newcomounsafe - Criar uma arena vazia por si só não gera perigo, mas o risco real está na indexação de arrays, que é realizada com muita frequência
- Safe Rust é eficaz sob a premissa de que o
unsafeé pequeno e isolado, mas, quando ounsafeé amplo como no Roc, essa premissa não se sustenta
O ecossistema Zig se encaixou bem no Roc
- Para o Bun, por causa da interoperabilidade entre JavaScript e memória manual, o
Dropdo Rust, que executa código de limpeza apenas uma vez, foi útil - No Roc, ao contrário, a intenção era usar arenas separadas para cada módulo e etapa de compilação; por isso, pacotes Rust que pressupunham um alocador global e
Dropimplícito eram inconvenientes - No ecossistema Zig, APIs que recebem explicitamente o alocador são comuns, o que combina bem com o modelo de gerenciamento de memória do Roc
- O ecossistema Rust era mais adequado à estrutura desejada pelo Bun, enquanto o ecossistema Zig era mais adequado à estrutura desejada pelo Roc
-
Reutilização do serializador de bitcode do LLVM
- O LLVM é uma dependência central do otimizador do Roc e executa otimizações adicionais após as otimizações próprias do Roc
- O LLVM frequentemente quebra a compatibilidade de suas principais APIs, então atualizar versões exigia tempo e custo consideráveis
- O formato de bitcode serializado do LLVM é estável e retrocompatível; portanto, usar um serializador próprio permite escapar das mudanças na API C++
- Para isso, é necessário um serializador de bitcode escrito manualmente e separado das bibliotecas C++ do LLVM
- A implementação conhecida já existente estava no compilador Zig, e o novo compilador do Roc reutiliza esse código Zig
- A maior fonte de dependências que o Roc obtém do ecossistema Zig é o próprio compilador Zig, mais do que pacotes comuns
Recursos do Rust que fazem falta no Zig
- Na implementação do compilador, é necessário controle explícito de alocação, mas nos testes a alocação e liberação automáticas do Rust eram mais convenientes
- O alocador de testes do Zig detecta vazamentos de memória e também consegue detectar vazamentos no código Roc compilado
- Em contrapartida, em cada teste é preciso escrever corretamente
initedefer deinit; se qualquer um deles estiver errado, o teste falha por vazamento
- Embora o
comptimedo Zig se sobreponha ao polimorfismo paramétrico e ad hoc, esses dois tipos de polimorfismo fazem falta- O trait
Allocatordo Rust pode receberself - Implementações como o
ArenaAllocatordo Zig precisam receber um ponteiroanyopaquee depois fazer cast para seu próprio tipo
- O trait
- A ausência de campos privados em structs impede transformar em erro de compilação o acesso direto a campos que não deveriam ser acessados
- Em revisões de diffs de código, aparece apenas o acesso ao campo, e não a documentação da struct original; por isso é preciso conferir separadamente a cada vez
- Às vezes faz falta a consistência do Rust, em que funções, variáveis e constantes usam todas
snake_case unsafee o borrow checker também tinham custos, mas davam a segurança de permitir se preocupar com certos problemas apenas dentro de blocosunsafe- Isso não significa defender que o Zig deva adicionar os mesmos recursos
- No Zig, era comum descobrir código morto mais tarde do que no Rust
- As ferramentas embutidas do Zig e o
tidy.zigdo TigerBeetle também não detectavam parte do código morto - Código morto não é gerado no binário e não afeta o usuário, mas é desfavorável para a manutenção da base de código
- As ferramentas embutidas do Zig e o
- No Rust, upgrades de versões minoritárias e mudanças de edição foram, em geral, tranquilos
- Atualmente o Zig não busca retrocompatibilidade, e isso já era uma condição esperada, então não foi um grande problema; ainda assim, a experiência simples de upgrade do Rust foi melhor
Pontos positivos do Zig
- O Zig tem o apelo de um design subtrativo: reduz ferramentas familiares, como na programação funcional, mas obtém outras propriedades em troca
- Não há macros, e muitos problemas, incluindo polimorfismo paramétrico, podem ser resolvidos com
comptimeou funções comuns - É possível controlar o layout dos dados com precisão
- Tipos inteiros que não são potências de 2, como
u7eu5, podem ser usados sem tratamento de bits separado - Há suporte nativo a packed struct
- Funções podem ser inline no ponto de chamada, não no ponto de declaração
- Recursos que no Rust exigiriam crates baseadas em macros podem ser usados sem dependências adicionais
- Tipos inteiros que não são potências de 2, como
- A cadeia de ferramentas de build do Zig se mostrou adequada para gerar binários autônomos para Alpine Linux e WebAssembly
- Também lida com builds especiais em que builtins, equivalentes à biblioteca padrão do Roc, são compilados como blobs binários opacos e incluídos no executável final
- A Uber também usa a cadeia de ferramentas Zig na infraestrutura de build, mesmo sem usar a linguagem Zig
- No tratamento de erros do Zig, falhas de alocação no heap também são tratadas como erros comuns em espaço de usuário
- O Roc usa uma estratégia semelhante, em que erros se acumulam naturalmente por meio de tipos soma anônimos e payloads
- Essa abordagem do Zig e do Roc é preferida em relação ao
anyhow, aothiserrore ao tratamento padrão baseado emResultdo Rust - Para a sintaxe de propagação de erros, a preferência foi pelo
?pós-fixado do Rust em vez dotrydo Zig, e por isso o Roc também adotou o operador?pós-fixado - Considerando o projeto como um todo, incluindo APIs de alocadores granulares e código reutilizável para compiladores de alto desempenho, a escolha pelo Zig foi muito satisfatória
Próximos passos do Roc
- A versão 0.1.0 do novo compilador está planejada para o segundo semestre deste ano e será o primeiro lançamento numerado do Roc
- Mesmo antes do lançamento, é possível testar os builds Nightly, mas atualmente ainda há vários bugs, recursos incompletos e documentação inacabada
- A Roc Programming Language Foundation é uma organização sem fins lucrativos 501(c)(3) dos EUA, e as doações são usadas principalmente para remunerar contribuidores
- O andamento do desenvolvimento e perguntas futuras podem ser acompanhados no Roc Zulip
2 comentários
Opiniões no Lobste.rs
Em termos numéricos, 35 ms no Zig é quase 100 vezes mais rápido que 3,4 s no Rust, e o código também é cerca de 50% maior, mas na prática de desenvolvimento essa diferença parece um pouco exagerada
Se você não recompila o compilador a cada minuto, mas sim a cada 10 minutos, o tempo economizado é de cerca de 3 segundos, e 3,4 s já é rápido o bastante
Conforme a base de código cresce, o Rust pode ficar mais lento, mas o tempo de build frio na verdade piorou, e somando isso ao fato de trocar de notebook a cada 2 anos e às melhorias no próprio compilador, é difícil afirmar que o tempo de build do Rust continuará aumentando no longo prazo
O modo
checkdo Rust e o LSP são mais rápidos que uma recompilação completa, mas não chegam ao mesmo nívelUsando a combinação
--watch -fincremental, dá para recompilar a cada salvamento e receber feedback de forma muito rápida e frequenteJá migrei um projeto pessoal de Rust para Zig e também usei Rust por anos no trabalho, e a velocidade de compilação do Zig foi realmente uma mudança que deu um grande alívio
35 ms é incrivelmente rápido
Só relincar já parece que levaria mais do que isso, então fico curioso sobre o que o compilador realmente está fazendo
Se você muda a implementação de uma função, basta inserir o novo assembly no binário existente, mas inlining, mudanças na assinatura da função, dependências e realocação quando falta espaço exigem análise adicional
Ao modificar ou adicionar uma função, ele encontra uma área livre grande o suficiente na seção
.textdo executável de saída e grava ali o novo código de máquinaSe faltar espaço, talvez seja preciso expandir a
.texte mover outros dados, mas as seções são ampliadas de forma exponencial para diluir esse custoQuando necessário, ele também adiciona a tabela de símbolos e entradas de realocação, deixando espaço livre antecipadamente para elas, então basta fechar o arquivo ao terminar o trabalho
Com base em um resultado do Tracy que vi recentemente, estimo que, dos 35 ms totais, o linker tenha usado cerca de 1 ms
O número de cerca de 1.200 usos de
unsafeem 300 mil linhas de código Rust é maior do que eu esperavaPor exemplo, o Inko tem apenas 162 expressões
unsafe { ... }e 87 funçõesunsafeexpostas ao código gerado via ABI C, e todo o código Rust soma cerca de 88 mil linhasAinda assim, como todo o backend LLVM é na prática uma enorme área
unsafepor causa de como o Inkwell funciona, é difícil tirar uma conclusão com uma comparação simplesEu marco como
unsafeapenas funções que realmente comprometem a segurança de memória, como mexer diretamente com ponteiros, mas outros desenvolvedores também usam isso como dica de que certa operação precisa ser feita antes para evitar pânicoO número de 40 mil ocorrências de
unsafena biblioteca padrão e no compilador Rust está incorretoEsse total contou até a palavra aparecendo em testes e comentários; a maior parte está na biblioteca padrão, e no compilador em si, mesmo incluindo comentários e testes, há menos de 2 mil ocorrências
Durante o tempo em que contribuí com o rustc, menos de 1% dos meus PRs continham
unsafe, a ponto de código inseguro dentro do compilador ser raroÉ natural que haja muito
unsafena biblioteca padrão, que implementa o runtime básico do qual todo Rust depende, e esse tipo de código é inseguro em qualquer linguagem, de forma explícita ou implícitaEsse compilador é cerca de 10 vezes menor que o rustc, mas usa uma quantidade parecida de
unsafe, então talvez não dê para dizer que isso é generalizado, mas ainda assim aparece com frequência, e fico curioso sobre por que ele precisa de muito maisunsafedo que o rustcNão separamos a análise do
unsaferelacionado à biblioteca padrão em nenhuma das duas bases, mas, se tivéssemos implementado o novo compilador em Rust, espero que ele tivesse maisunsafeque o rustc por causa do cache e da estrutura que usa índices em vez de ponteirosA intenção não era diminuir o trabalho da equipe do Rust, mas explicar nossas escolhas e o andamento do projeto, respeitando os resultados de outros projetos
É um texto que avalia de forma equilibrada, sem esconder os prós e contras de Rust e Zig, e foi especialmente honesto ao concluir que, mesmo depois de 18 meses escrevendo centenas de milhares de linhas e lidando com centenas de bugs, o resultado do projeto provavelmente não teria mudado muito se tivessem escolhido outra linguagem entre as opções
Ainda assim, é difícil entender a explicação de que, em compiladores que geram código de máquina como o Roc ou o rustc, operações de memória inseguras representam uma grande parte do trabalho
Há muitos compiladores escritos em OCaml ou Haskell, e gerar código de máquina em si é apenas montar bytes em um vetor e gravá-los em um arquivo, então não há motivo para isso ser inseguro
Se fosse interpretação ou compilação JIT, eu entenderia, mas fico curioso sobre por que isso seria necessário também na compilação comum
O risco surge ao executar o código de máquina gerado, e como compiladores reais muitas vezes geram e executam código de máquina ao mesmo tempo, foi dito que isso representa uma grande parte do trabalho
Além de interpretação e JIT, isso inclui avaliar código do usuário em tempo de compilação, como em
const fndo Rust ou expressões que podem ser elevadas ao topo no Roc, ou executar testes e então inspecionar a saída para decidir o que mostrar ao usuárioSeja a memória corrompida no processo do compilador ou no programa gerado, o critério é que a causa do comportamento incorreto do processador foram instruções produzidas pelo compilador, e o lugar a ser corrigido também é o código do compilador
Componentes como código de ligação externa e o coletor de lixo também exigem muito
unsafeem Rust, o que anula em boa parte as vantagens do Rust seguroDá para escrever só o compilador em uma linguagem segura, mas desempenho é um problema; tanto o Zig quanto o compilador do Roc usam bastante estrutura de arrays (SoA) e índices de arrays em vez de ponteiros
Implementar isso em Rust acaba contornando o borrow checker e fazendo perder as vantagens de segurança relacionadas
As relações de tempo de vida no compilador são surpreendentemente simples: em uma fase, aloca-se, cria-se e modifica-se dados em uma arena; na fase seguinte, eles são passados como somente leitura; depois disso, pode-se descartar a arena inteira
As partes mais complexas são a compilação incremental e o linking, que leem estado do disco e modificam binários no lugar; nesse processo, pode haver corrupção de estado, bugs, compilação incorreta e problemas de memória, mas isso é separado da segurança de memória do próprio processo do compilador
Segurança e correção são conceitos muito mais amplos do que a segurança de memória dentro do programa, especialmente quando se quer executar de forma segura e correta tarefas que são difíceis de expressar em Rust
Fiquei feliz em ver a menção a mim como criador do
compact_arena, mas a forma como o uso deunsafenessa biblioteca foi descrito está erradaO objetivo do
compact_arenaé garantir que não seja possível usarnewincorretamente em Rust seguro, tornando a indexação seguraA macro
mk_arenapode ser chamada em código Rust seguro mesmo dentro de loops em que não se sabe quantas arenas serão necessáriasnewé usado com segurança, mas na seção de segurança de https://docs.rs/compact_arena/0.5.0/… está escrito que a tag passada ao construtor é a base da técnica de indexação, e que usá-la em outra arena pode misturar índices de duas arenas, levando a acesso fora dos limites e comportamento indefinidoMeu ponto não era a chamada a
new()em si, mas que o risco surge ao indexar valores criados pornew(), e considero que o lugar a ser auditado e marcado comounsafeseria a chamada de indexação em siMas é possível que eu tenha entendido o design de forma errada
Pelo que sei, em Python é preciso reiniciar o programa para refletir código novo, e hot reloading exige extensões não padronizadas e escrita cuidadosa do código
Hot reloading é mais comum em ambientes de desenvolvimento baseados em imagem, como Lisp ou Smalltalk, ou em Erlang
importlib.reload(), hot reloading também é possível em Python, mas não sei o quão amplamente isso é usadoFico curioso se deixar de usar
cargo buildpode reduzir o tempo de buildNão está claro se o trabalho de configurar o Bazel seria maior ou menor do que portar para Zig, e claro que o tempo de build não foi o único motivo para a troca de linguagem
Comentários do Hacker News
No geral, o texto é bom, mas é difícil concordar com a afirmação de que, em compiladores que geram código de máquina como Roc ou
rustc, operações inseguras de memória seriam uma grande parte do trabalhoPatching de binário em execução ou recarregamento de código talvez precisem de código inseguro, mas no processo normal de gerar um executável não há motivo para que a própria geração de código de máquina seja insegura
Na verdade, é mais provável encontrar código inseguro no runtime da linguagem
Na prática, porém, muitos compiladores também executam diretamente o código de máquina enquanto o geram, então usei a expressão “uma grande parte do trabalho”, mas o compilador não precisa necessariamente fazer as duas coisas
Eu tinha em mente não só patching de binário, recarregamento de código e runtime, mas também avaliar código do usuário em tempo de compilação, como
const fnno Rust ou expressões que podem ser içadas para o topo no Roc, além de executar testes e inspecionar sua saída para decidir o que exibirÉ uma lógica parecida com dizer que, como ainda é possível bater a cabeça usando cinto de segurança, o cinto é só um incômodo e não deveria ser usado
Em código de altíssimo desempenho, mudam desde as estruturas de dados e algoritmos até a estratégia de alocação de memória, e o TigerBeetle é famoso por alocar toda a memória de uma vez na inicialização
Como o compilador do Roc também pretende adotar concessões semelhantes às do Zig, é natural que apareçam vários padrões em comum
Não parece haver base para a alegação de que
ReleaseSafepega use-after-free com verificações em tempo de execuçãoProcurei na documentação do Zig sobre verificações de segurança de memória em runtime, mas não encontrei nada relevante buscando por
use-after-free,UaFousafety-checked, e mesmo usandoDebugAllocatorem builds de release isso não detecta o problema de forma confiávelO assunto está resumido em https://landaire.net/memory-safety-by-default-is-non-negotia...
É uma avaliação formada escrevendo código diretamente, antes da IA; talvez algo mude na combinação de Zig com LLMs, mas quanto mais leio o texto, mais vejo afirmações estranhamente formuladas, o que dificulta confiar nele
Soa menos como um julgamento técnico sincero e mais como um texto escrito para justificar uma discussão já existente, mas gosto de textos e linguagens incomuns e tenho certa antipatia pela empolgação excessiva com IA, então por enquanto vou presumir boa-fé
ReleaseSafebasicamente adiciona verificações de limites e faz panic em código inalcançávelO Zig aparentemente não oferece segurança temporal de memória (temporal memory safety)
É interessante que o OCaml, já maduro, tenha sido usado para validar protótipos por ser flexível e suficientemente expressivo, mas não tenha sido escolhido como linguagem da implementação final
Também fica a dúvida se o build incremental do Zig é de fato significativamente mais rápido que o dune, e “por que Zig?” nem sequer abordou a vantagem da compilação cruzada
Gostaria de entender se controle fino de memória é realmente tão importante em um compilador e em que marco mantenedores decidem migrar para outra linguagem, como aconteceu com Rust e WASM, que começaram em OCaml
O build incremental do Zig é claramente um recurso decisivo, e dá para entender a escolha de trocar de linguagem para obter isso no curto prazo
Mas, no médio prazo, espero que o Rust também ganhe algo parecido em um futuro próximo
Quero velocidade, mas não quero acelerar tanto a ponto de me prejudicar; estou criando minha própria linguagem para combinar a segurança do Rust, os recursos do Zig e um runtime de Go sem garbage collector
Isso parece mais viável, e talvez até dê para implementar em espaço de usuário
Teria sido muito mais convincente se tivesse havido uma comparação científica de fato no processo de escolha da linguagem de implementação do compilador
Parece que partiram da suposição não verificada de que um compilador de alto desempenho precisa de uma linguagem de sistemas de baixo nível (https://www.roc-lang.org/faq#self-hosted-compiler) e concluíram que, fora Rust, a única opção era Zig
O desempenho de compiladores é dominado por algoritmos, e até linguagens gerenciadas rápidas, usando os mesmos algoritmos, em geral ficam com tempo de execução dentro de no máximo o dobro, enquanto a diferença de desempenho causada por algoritmos não tem um limite comparável
O próprio Zig é um contraexemplo à teoria de que escrever um compilador em uma linguagem de baixo nível o tornaria mais rápido, e as cerca de 15 mil linhas por segundo do Roc não são rápidas. Há material mostrando que um compilador ML já processava 3 mil linhas por segundo em 1998 (https://flint.cs.yale.edu/cs421/case-for-ml.html)
Talvez fosse mais útil para o futuro parar o trabalho atual no compilador e criar um compilador self-hosted para um subconjunto pequeno de Roc, possível em menos de 10 mil linhas
Assim, em vez de uma implementação com 300 mil linhas, seria possível testar várias implementações na escala de 10 mil linhas e verificar se uma linguagem de baixo nível é realmente necessária para atingir as metas de desempenho
No processo de self-hosting, as funcionalidades de Roc que realmente importam apareceriam, mais código em Roc seria escrito e, ao melhorar os recursos de uso geral necessários ao compilador, as aplicações abaixo dele também melhorariam
Só o fato de ML compilar rápido nos anos 1990 não basta para julgar a velocidade de compilação do Roc hoje. O design da linguagem impõe restrições fortes aos algoritmos necessários, e o hardware moderno também é muito mais complexo
O Roc parece ter algum grau de sobrecarga e algoritmos sofisticados para evitar alocar closures no heap, e essas exigências podem criar uma complexidade algorítmica inevitável
Quando se chega ao limite da otimização algorítmica, o que resta é reduzir fatores constantes e, especialmente em linguagens de alto nível com gerenciamento de memória, existe claramente um limite inferior para o quanto isso pode ser reduzido
Já vi casos em código real em que controlar diretamente o layout de memória trouxe melhoria de mais de 10x no desempenho, e na indústria de games esse tipo de trabalho às vezes ocupa grande parte da carreira. A ideia de que um algoritmo esperto resolve todos os problemas de desempenho está longe da realidade
Builds de Rust são uma das grandes causas de desperdício de espaço em disco em praticamente qualquer computador, e ao compilar várias bibliotecas é fácil acumular dezenas de GB
Dá para configurar uma pasta global de build para reutilizar dependências entre projetos, mas seria melhor que qualquer solução desse tipo viesse como comportamento padrão
Ainda assim, coleta de lixo de cache é necessária, e está na fase final um novo trabalho de reorganização dos artefatos intermediários de build para facilitar isso
node_modulesé sempre criticado no ecossistema JavaScriptMesmo em um projeto Tauri onde o código de backend é muito menor que o de frontend, os artefatos de build de Rust chegam a 9 GB, enquanto
node_modulesfica em apenas 550 MBEntendo Roc como uma linguagem de script embutida em uma C ABI, e tenho curiosidade sobre os casos de uso reais
Fico pensando se a ideia é competir com WASM em ambientes de plugin que oferecem uma grande plataforma Roc e por que um desenvolvedor de aplicações teria motivo para expor uma camada Roc em vez de usar WASM, que permite aos autores de plugins usar qualquer linguagem
Se for uma linguagem no nível da aplicação usando uma pequena plataforma Roc, também tenho curiosidade se a proposta é competir com Gleam no código HTTP do lado do servidor e com Elm no código cliente
Tempo de compilação é um fator muito subestimado. Ter de esperar 10 minutos por um build de C++ é minha maior reclamação, e isso quebra completamente o fluxo de desenvolvimento
A diferença ao trocar de um arquivo
.rspara um.tsé tão grande que parece que troquei o próprio computadorNão uso Zig diretamente, mas tenho grandes expectativas para algumas possibilidades
Um novo jogo escrito em Zig, software distribuído em que TigerBeetle já mostrou potencial e a área de robótica, que pessoalmente me interessa, são os pontos que mais me animam