- Tree Borrows, um novo modelo de memória para superar os limites de otimização de código
unsafe na linguagem Rust, foi proposto
- O Tree Borrows resolve com uma estrutura em árvore o problema de o método Stacked Borrows existente não permitir vários padrões frequentemente usados em código Rust de produção, oferecendo regras mais realistas e flexíveis
- O Tree Borrows passa em 54% mais casos de teste de código do mundo real do que o Stacked Borrows
- Ao mesmo tempo, preserva a maior parte dos principais aspectos de segurança de memória e potencial de otimização do Rust, especialmente read-read reordering, além de refletir recursos avançados do borrow checker mais recente do Rust
- Com a introdução de um modelo de máquina de estados baseado em árvore, apresenta um marco importante para a pesquisa em otimização de Rust e verificação de segurança
O sistema de ownership do Rust e os limites do código unsafe
- O Rust fornece garantias fortes, como segurança de memória e prevenção de data races, por meio de um sistema de tipos baseado em ownership
- No entanto, o Rust possui uma unsafe escape hatch e, nesse caso, a responsabilidade pela verificação de segurança deixa de ser do compilador e passa para o desenvolvedor
- O compilador quer aproveitar regras de alias de ponteiros para aplicar otimizações agressivas, mas código
unsafe incorreto pode inviabilizar essas otimizações
Stacked Borrows e suas limitações
- Até então, o modelo Stacked Borrows definia o “comportamento incorreto” em código
unsafe e fornecia um critério para otimização
- Porém, essa abordagem não permite vários padrões
unsafe comuns em código Rust real e também não reflete recursos do borrow checker introduzidos recentemente no Rust
O surgimento do Tree Borrows
- O Tree Borrows é um novo modelo que rastreia permissões de memória com uma estrutura em árvore em vez de uma estrutura empilhada
- Com isso, permite com segurança mais padrões de código Rust usados na prática, aumentando bastante a flexibilidade das regras de borrow e sua aplicabilidade no mundo real
- Em uma avaliação com 30.000 crates populares de Rust, passou em 54% mais casos de teste do que o Stacked Borrows
Características e vantagens do Tree Borrows
- Mantém a maior parte das otimizações principais do Stacked Borrows existentes, como read-read reorderings
- Além disso, consegue refletir recursos avançados do borrow checker mais recente do Rust, como padrões de borrow não estruturados e manipulações complexas de ponteiros
- Introduz um modelo de máquina de estados baseado em árvore para equilibrar segurança e potencial de otimização
Conclusão e significado
- O Tree Borrows apresenta um novo padrão de referência para o tratamento e a pesquisa de otimização de código
unsafe no compilador Rust
- É avaliado como um modelo de memória realista e robusto que abrange tanto código Rust de produção quanto as políticas mais recentes do borrow checker
- O artigo, os artefatos e o código-fonte relacionados foram publicados e devem ter grande impacto na comunidade de pesquisa sobre compiladores Rust e verificação
1 comentários
Comentários do Hacker News
O post recente no blog de Ralf Jung dá mais contexto link
Bônus: também recomendo a apresentação recente do grupo de pesquisa que está tentando especificar claramente a semântica de execução do Rust em um dialeto de Rust YouTube
Há a alegação de que, do ponto de vista do compilador, as garantias fortes do sistema de tipos sobre aliasing de ponteiros permitem otimizações poderosas, mas fico curioso sobre o quanto isso é realmente eficaz na prática
Linus Torvalds há muito tempo argumenta que a regra de strict aliasing do C tem pouca utilidade e, em vez disso, causa problemas
O texto dele com exemplo também é interessante
Fico pensando se Rust é essencialmente muito diferente de C nesse aspecto, mas pela minha experiência pessoal, especialmente quando
unsafeentra na jogada, não parece tão diferente assimAcho que a regra de strict aliasing do C é realmente bem ruim
As regras propostas no Rust são bem diferentes, mais úteis do ponto de vista do compilador e menos pesadas para o programador
Há um opt-out dentro da própria linguagem com o uso de raw pointers, e também existem ferramentas para verificar o código
No fim, como todo design de linguagem, é um compromisso
Parece que Rust encontrou um novo equilíbrio nessa área de otimização, e o tempo dirá como essa escolha vai se sair
As regras de aliasing do Rust são bem diferentes das de C
Em C, a palavra-chave
restrictpraticamente só faz sentido em argumentos de função, e aliasing baseado em tipo (type-based aliasing) na prática quase não é usado ou é incômodo de usarEm Rust, lifetimes, mutabilidade e outras formas refinadas de expressar acesso à memória permitem tratá-la com segurança de várias maneiras, independentemente do tipo em si
O fato de só não poder haver referências
&mutsobrepostas, enquanto ainda é possível dividir em várias&mutsem sobreposição, é um ponto importanteGostaria de ver uma análise mais ampla sobre quanto isso realmente impacta o desempenho
Daria para descobrir rapidamente removendo do compilador toda a parte que passa informações de aliasing ao LLVM e comparando a performance
Também há a alegação de que a anotação
noaliasmelhora em cerca de 5% o desempenho em runtime; há um comentário relacionado sobre isso (embora os dados já estejam meio antigos)Convém filtrar o que o Linus diz sobre compiladores
Kernel de sistema operacional e compiladores são áreas completamente diferentes
Hoje em dia, análise de aliasing é realmente uma peça central para ganhos fortes de desempenho
Os maiores efeitos vêm de heurísticas simples, e consultas complexas de aliasing por si só costumam ter utilidade limitada
Eu adoraria testar experimentalmente quanto uma análise de aliasing teoricamente perfeita melhoraria a performance, mas imagino que o limite em código comum seja algo como 20%
Claro, otimizações muito avançadas, como transformação de layout de dados, têm a limitação de nem sequer serem tentadas sem análise de aliasing
O strict aliasing de C e o aliasing de Rust são conceitos diferentes
Em C, o centro da questão é a análise baseada em tipo (TBAA), e Rust deliberadamente não adotou isso
Já houve threads antigas sobre Stacked Borrows em 2020 e 2018
Thread de 2020
Thread de 2018
Li a especificação de Tree Borrows no site do Nevin há alguns anos, e fiquei impressionado com a forma elegante como ela resolve problemas complexos
Na minha experiência, Tree Borrows permite código razoável que não seria possível com Stacked Borrows
Também vale ver este exemplo de código da biblioteca padrão do Rust
Fico curioso se Rust ou linguagens PL de próxima geração podem evoluir para permitir escolher entre várias implementações de borrow checker, com características e objetivos diferentes, como velocidade de compilação, velocidade de runtime, flexibilidade algorítmica etc.
Rust já suporta trocar a implementação do borrow checker
Ele saiu de um modelo baseado em escopo para o modelo non-lexical, e também existe a implementação experimental chamada Polonius como opção
Quando uma nova implementação fica pronta, a antiga não é mantida por necessidade
Também dá para usar formas mais flexíveis com checagens em runtime, como
RceRefCellJá existem várias abordagens, como affine types (usado por Rust), linear types, sistemas de efeitos, dependent types e prova formal
Cada abordagem tem características diferentes em custo de implementação, desempenho, experiência de desenvolvimento etc.
Mesmo fora do Rust, há uma tendência de buscar combinações entre gerenciamento automático de recursos e sistemas de tipos
O que realmente seria necessário é separation logic, capaz de especificar com precisão as preconditions de uma função e até provar condições intermediárias
A abordagem do Rust é sistematizar invariantes comuns que as pessoas realmente querem e, com isso, garantir otimizações fortes
Fico me perguntando se o resultado do borrow checker só tem false negatives e não false positives
Se for assim, talvez desse até para rodar várias implementações em paralelo em threads e usar o resultado da que terminar primeiro
Permitir várias implementações de borrow checker ao mesmo tempo provavelmente não seria desejável, porque o ecossistema tenderia a se fragmentar
Testei de fato o código Rust citado no artigo e confirmei que ele não é rejeitado pelo compilador estável mais recente
Código de exemplo:
Se você executar o código acima no miri, ele reporta erro em
*x = 10;, mas não ocorre erro emwrite(x);rustc, do ponto de vista do sistema de tipos, não tem motivo para rejeitar nenhuma das duas versões, porque
yé um*mutO artigo usa o exemplo abaixo como problema de código
unsafe:Fico em dúvida se isso é realmente possível
Usar referências mutáveis múltiplas para a mesma variável via ponteiros é claramente UB, então imagino que devo estar entendendo algo errado
O compilador Rust aceita esse código, mas ele ainda viola as regras
Quais regras?
Código que passa no borrow checker é legal
unsafetambém pode expressar coisas ilegais/UBExiste um conjunto de regras mais amplo do que o escopo do borrow checker, mas que ainda assim continua sendo legal
O objetivo dessa pesquisa é especificar esse limite com rigor
O artigo sobre Stacked Borrows é mais simples, mas tinha limitações para código
unsafereal; Tree Borrows reconhece uma faixa mais ampla de segurançaEstá claro que “não podem existir vários ponteiros para referências mutáveis ao mesmo tempo”, mas não há uma regra explicitada com precisão dizendo exatamente qual regra isso viola
Tree Borrows propõe justamente esse tipo de definição
Quando se diz “o código pode fazer esse tipo de coisa”, isso significa que você de fato pode escrever e executar esse código, mas sem uma definição como Tree Borrows fica difícil fundamentar por que exatamente isso está errado
Parece que você mesmo já aceita a necessidade de regras claras como as do Tree Borrows
Código
unsafeassim é realmente possível na prática, e é justamente isso que o torna UBExemplo: link do playground
Se quiser entender o contexto, o começo do parágrafo logo seguinte no artigo mostra bem a intenção
Sim, esse é exatamente o ponto
É difícil impor na prática a regra que proíbe múltiplas referências mutáveis, e
unsafepermite muito mais do que aquilo que o sistema de lifetimes do Rust garanteUm dos autores, Neven Villani, é filho de Cédric Villani, vencedor da Medalha Fields de 2010
Vem à mente a metáfora de que a maçã não cai longe da árvore
E também dá vontade de brincar dizendo que “as qualidades também foram emprestadas da árvore”
Eu já tive o escritório perto do do pai dele (vencedor da Medalha Fields)
Isso foi antes de ele entrar para a política
Esse modelo é realmente excelente
Pretendo implementá-lo na linguagem que estou criando
Não pode ser déjà vu
Tenho a sensação de ver este post de novo a cada 2 ou 3 meses