- O protótipo de empréstimo por região do Vale conseguiu compilar com sucesso pela primeira vez, permitindo validar em programas reais uma abordagem de segurança de memória que combina referências geracionais e regiões
- Desenvolvedores podem escrever código de forma próxima de C/C++, aplicando pure e empréstimo por região apenas onde for necessário para reduzir a sobrecarga das verificações geracionais
- O primeiro programa zero-check em Vale foi um exemplo de Cellular Automata para geração de níveis de roguelike, e o assembly resultante chegou a um nível quase idêntico ao modo
unsafe_with_bounds
- Nos benchmarks,
safe_fastest não apresentou slowdown observável em relação a unsafe_with_bounds, enquanto unsafe_no_bounds foi 1.18 ± 0.01 vezes mais rápido que os dois modos
- Ainda não se trata de uma comparação direta com C/Rust; ainda restam um pre-optimizer específico para Vale e a organização dos recursos de região por causa de ruído de otimização do LLVM, maturidade do protótipo e falta de suporte a inline data
Combinação de referências geracionais e empréstimo por região
- A abordagem de segurança de memória do Vale busca não usar reference counting, tracing garbage collection nem borrow checking
- A estrutura básica é que o desenvolvedor escreve o programa de forma próxima de C ou C++, e as generational references do Vale mantêm a segurança de memória
- Depois, ao aplicar pure e region borrowing, é possível remover a maior parte da sobrecarga das verificações geracionais
- Com o estilo linear adicionado, acredita-se que as verificações geracionais no código Vale possam cair a zero
- O empréstimo por região é totalmente opt-in, então é possível primeiro escrever com mais liberdade e depois adicionar isso apenas às partes que precisarem de otimização
- A proposta é permitir que, dentro do mesmo programa, algumas partes sejam flexíveis como Java, outras rápidas como Rust, ou qualquer ponto intermediário
O que foi necessário para criar o protótipo
- Nos últimos anos, a base do compilador foi construída para dar suporte ao mesmo tempo ao sistema de empréstimo por região e às generational references
- Além da complexidade do próprio sistema de empréstimo, também foi necessário ter full generics mais fortes do que os templates existentes
- Para que regiões e referências geracionais funcionassem juntas de forma natural, também foi preciso adicionar uma nova etapa ao compilador
- Internamente, as regiões são reduzidas a inteiros de “pure height”
- O parâmetro genérico de região é representado por um número negativo, a região padrão por 0, e cada bloco pure por números positivos crescentes
- Há alguns meses, o protótipo de regiões ficou pronto e, embora ainda esteja bruto, conseguiu compilar algo com sucesso pela primeira vez
- Como resultado, foi criado o primeiro programa zero-check em Vale
- A flag do compilador
--print_mem_overhead true pode contar o número de verificações geracionais do programa
O primeiro programa zero-check e a comparação de assembly
- O primeiro programa foi um exemplo de Cellular Automata para gerar níveis de um jogo roguelike
- Mesmo pequenos erros no código do compilador podem adicionar instruções extras ao assembly resultante e criar uma sobrecarga artificial no programa final
- Para rastrear isso, o assembly resultante foi comparado continuamente com os modos unsafe do Vale
unsafe_no_bounds: desativa todas as proteções de segurança de memória, de forma semelhante a C, e usa raw pointers em vez de generational references
unsafe_with_bounds: adiciona bounds checking em acessos a arrays, como em Rust
- Depois de rastrear as diferenças por alguns meses, o assembly resultante ficou quase idêntico ao modo
unsafe_with_bounds
- A única diferença esperada era inserir um número de geração pseudoaleatório no topo de cada allocation, e ele não era lido nas verificações geracionais reais
- Internamente, é usado um registrador monotonicamente crescente para manter isso rápido
- Essa diferença pode ser removida quando isolates ou unique references forem adicionados
Resultados dos benchmarks e condições de medição
- O resumo do benchmark é o seguinte
Summary
'./build_unsafe_no_bounds/main' ran
1.18 ± 0.01 times faster than './build_unsafe_with_bounds/main'
1.18 ± 0.01 times faster than './build_safe_fastest/main'
- O modo normal do Vale,
safe_fastest, não mostrou slowdown em comparação com o modo que tem apenas bounds checking
- Nessa medição, essa abordagem teve sobrecarga não observável
- Para testar diretamente, é possível compilar a branch regions, consultar os scripts de benchmarking e tirar dúvidas no servidor do Discord
- As condições da medição têm limitações claras
- Não é um benchmark com comparação direta com linguagens como C ou Rust
- Esses compiladores têm anos de otimizações independentes, o que pode confundir as variáveis do experimento
- Para isolar a diferença da abordagem de segurança de memória, a comparação foi feita com
unsafe_no_bounds e unsafe_with_bounds
- O ambiente de medição foi um Razer Blade 15" 2018 com SSD de 512GB rodando Ubuntu 22.04
- A ferramenta de medição foi o hyperfine, executado dentro de um cset shield
Ruído de otimização observado em programas maiores
- Em programas maiores, foi observado bastante optimizer noise
- Isso é diferente de benchmark noise; a configuração de medição mostrou tempos de execução muito consistentes, como
± 0.01
- Pequenas mudanças em uma área podiam puxar a medição para um lado
- Houve até um caso em que, ao mudar o tamanho do generation number, apareceu de forma consistente um overhead negativo de
1.13 ± 0.01
- Isso foi estranho porque o programa não tinha muitos generation numbers
- É possível que mudanças na alocação de registradores tenham dominado a diferença de desempenho que viria da mudança semântica
- Em um programa maior, um pequeno jogo roguelike, o optimizer não conseguiu fundir dois branches idênticos dentro de um if e também deixou passar outras otimizações óbvias
- Não está claro qual impacto a existência de um inteiro não lido causa, e isso também pode ser um bug do LLVM
- Isso sugere que talvez seja necessário um pre-optimizer específico para Vale, semelhante ao MIR do Rust
- O LLVM foi projetado mais pensando em C
- Se o LLVM interpretar generational references como um padrão de acesso intencional a memória desalocada, pode tratá-lo como undefined behavior
Aplicabilidade e próximos passos
- Referências geracionais e regiões podem, quando combinadas, criar uma abordagem de segurança de memória muito rápida
- Os domínios de software em que essa abordagem pode se encaixar bem têm as seguintes características
- desejam latência mais previsível do que tracing garbage collection
- desejam melhor desempenho e cache friendliness do que reference counting
- querem prototipagem e iteração mais fáceis do que com borrow checking
- Ainda há trabalho restante antes de o Vale poder ser comparado de frente com C ou C++
- O optimizer do LLVM tem dificuldade para inferir generation e imutabilidade, então é necessário um pre-optimizer específico para Vale
- O Vale precisa oferecer suporte a inline data em vez da solução temporária atual, que coloca todos os structs no heap
- Como o benchmark acima não usou structs, a falta de suporte a inline data não afetou esse resultado
- Os recursos de região ainda estão em fase de protótipo, então é preciso aparar as arestas e reduzir a dívida técnica antes de mesclá-los à branch principal
- Depois da mesclagem, o plano é fazer a biblioteca padrão usar regiões, para que o código principal do programa do usuário se beneficie disso mesmo sem usar regiões diretamente
- A medição atual mostra que programas zero-check são possíveis e podem alcançar a velocidade esperada
1 comentários
Comentários do Hacker News
Baixei o Vale para testar, mas tive uma má impressão quando executei o compilador
valecpela primeira vez sem argumentos e ele imediatamente imprimiu "(panic)"panicé uma expressão muito forte, e acho que deveria ser evitada em situações normais de tratamento de erro. Quando um programa entra em pânico, parece uma situação fora de controle, o que deixa uma sensação ruimEm seguida tentei ver a ajuda dos argumentos de linha de comando, mas na prática ela não existia; salvei o exemplo Hello World do site em
hello.vle executeivalec hello.vl, e apareceuUnknown subcommandEntão executei
valec build hello.vl, mas depois deUnrecognized input: hello.vlapareceu novamente(panic), evalec helptambém não ajudou, então acabei desistindo. Não faço ideia de como usar issocatdiretamente novalec-help-build.txtincluído no download, vai encontrar a explicação do que está procurandoO compilador ainda está bem bruto nas bordas neste momento. De agosto até maio, estive 100% focado em prototipar regions, e o que você está vendo agora é a dívida técnica acumulada durante esse processo. Isso inclui a falta de testes de integração para o sistema de ajuda
Nos últimos 1~2 meses venho pagando essa dívida, mas ainda não consegui voltar ao nível em que estava no lançamento 0.2. Se precisar de mais ajuda, me avise ou apareça no servidor do Discord, onde há muita gente que pode ajudar
Dito isso, o README não deixa isso claro e diz “Try Vale”, então fica ambíguo. Mesmo assim, no momento parece mais próximo de P&D/prova de conceito
Mesmo do ponto de vista de interface ou bugs, a experiência de depurar C++ de 40 anos com um gdb de 35 anos compete tranquilamente com qualquer linguagem experimental. Por exemplo, exibir
funcname()::staticvarnameé uma interface estranha e falha metade do tempo. Sem nem falar dos sistemas de build de C++Se é uma tecnologia experimental, dá para criticar o conceito, mas acho aceitável relevar uma interface meio áspera
https://github.com/ValeLang/Vale#building-a-vale-program
O fato de ter latência mais previsível do que coleta de lixo por rastreamento, melhor desempenho e melhor afinidade com cache do que contagem de referências, e ser mais fácil para prototipar e iterar do que verificação de empréstimos, já me deixou mais do que curioso — fiquei interessado de verdade
Até comecei a assinar o feed RSS: https://verdagon.dev/rss.xml
O Vale precisa de mais apoiadores
https://github.com/sponsors/ValeLang
Espero que, enquanto este post estiver na primeira página, o projeto consiga atingir a meta de US$ 3.000 por mês
Quero ajudar o Evan a poder trabalhar nisso em tempo integral. Eu também sou apoiador. Uma linguagem rápida, segura e divertida para prototipar merece apoio
“Um pré-otimizador específico do Vale, algo parecido com o Cranelift do Rust” provavelmente se refere ao MIR, ou seja, representação intermediária de nível médio. Há um bom post no blog sobre isso: https://blog.rust-lang.org/2016/04/19/MIR.html
O Cranelift é um backend de compilador focado principalmente em JIT, mas em teoria também poderia substituir o LLVM. O trabalho em um backend alternativo também está em andamento, mas com limitações: https://github.com/bjorn3/rustc_codegen_cranelift
Uma abordagem que permite não se preocupar com gerenciamento de memória na maior parte do código, enquanto dá a opção de otimizar apenas os caminhos quentes com abstrações de custo zero, parece reunir o melhor dos dois mundos
Principalmente se, por conveniência, o trade-off for só de desempenho e não de segurança
Propriedade compartilhada é um conceito ruim, então também não uso smart pointers
Acho que problemas de gerenciamento de memória geralmente são triviais
Continuo me perguntando o que significa dizer que isso é seguro no contexto de referências geracionais
Se entendi direito, isso quer dizer que evita use-after-free e double-free? Se for isso, o programa ainda pode falhar ao acessar a memória quando a geração esperada não corresponde à geração real
Nesse sentido, parece menos seguro do que contagem de referências, coleta de lixo por rastreamento ou verificação de empréstimos
Se você tentar acessar memória liberada por meio de uma referência, isso deve resultar de forma previsível e segura em um erro de segmentação ou falha de asserção. No futuro, esperamos introduzir uma melhoria com remapeamento do espaço de endereços virtuais que pode até eliminar os erros de segmentação
Dito isso, se for um índice geracional, o runtime também deveria conseguir verificar se o acesso é válido antes de tentar o acesso de fato. Não sei se isso é possível no Vale
malloc/freee tem outras vantagensA função
checkprecisa do número de geração da alocação, então ela acessa a alocação. Ou seja, para verificar se a referência pode acessar aquela alocação, primeiro ela precisa acessar essa alocaçãoClaro, se a alocação já tiver sido liberada, então o próprio ato de acessar a alocação e seu número de geração já é comportamento indefinido, então isso não funciona
Parece tão óbvio que não sei se estou deixando passar algo importante, ou se “segurança de memória” aqui está sendo usado em um sentido completamente diferente
Vale não é uma linguagem como V. V recebeu uma análise muito crítica em https://mawfig.github.io/2022/06/18/v-lang-in-2022.html, e eu estava confundindo as duas por causa da semelhança dos nomes
Estou deixando registrado caso mais alguém tenha cometido o mesmo erro
O conteúdo do texto já não é mais relevante, mas ele continua lá e também é o único post daquele blog
Agora já é 2023 e V também está em beta (0.4). Além disso, a pessoa que criou aquele texto usou uma conta descartável no GitHub para publicar a análise/ataque, causar polêmica e desaparecer
O único texto publicado naquele blog também é um ataque ao V, não há outras análises. As partes que tinham conteúdo prático já foram corrigidas[1]
Se você procurar por
mawfig.github, também verá que isso foi repetidamente espalhado no HN e geralmente usado para difamar[1]: https://github.com/vlang/v/issues/14803
[1]: https://github.com/vlang/v/issues/14787
[1]: https://github.com/vlang/v/issues/14786
Parabéns ao Evan por alcançar esse marco. Não tenho experiência com design de linguagens de programação nem com compiladores, mas gosto de ler os textos sobre Vale
Agora temos o Vale do Evan e o Val do Adobe Software Technology Lab, então pesquisar material relacionado provavelmente vai ficar bem difícil
https://www.val-lang.dev
“Vale é rápido: Vale é compilado AOT com LLVM, usa tipagem estática, usa a nova técnica de referências geracionais para segurança de memória com velocidade e flexibilidade, e em breve introduzirá verificação de empréstimos por regiões para ficar ainda mais rápido”
https://vale.dev/
Parece que estou ouvindo de fora uma discussão entre duas pessoas que já dura 5 anos
Alguém pode explicar o que está acontecendo aqui? O texto é difícil demais de acompanhar
Resumindo, Vale é uma linguagem parecida com um C++ mais limpo e usa referências geracionais[0], algo mentalmente parecido com executar com ASan[1] ativado
Referências geracionais têm algum overhead, mas isso pode ser removido com regiões[2], mais especificamente com empréstimo imutável por regiões[3]. Isso aproxima o Vale de seu objetivo de ser uma linguagem de alto desempenho mantendo segurança de memória
[0] https://verdagon.dev/blog/generational-references
[1] https://github.com/google/sanitizers/wiki/AddressSanitizer
[3] https://verdagon.dev/blog/zero-cost-borrowing-regions-overvi...
[4] https://verdagon.dev/blog/zero-cost-borrowing-regions-part-1...