1 pontos por GN⁺ 2026-05-06 | 2 comentários | Compartilhar no WhatsApp
  • Async Rust permite executar código independente de executor tanto em servidores quanto em microcontroladores, mas a máquina de estados gerada pelo compilador faz o tamanho do binário crescer de forma especialmente visível em sistemas embarcados
  • Mesmo um exemplo simples como bar(), com 2 pontos de await, gera 360 linhas de MIR e os estados Unresumed, Returned, Panicked, Suspend0, Suspend1, enquanto a versão síncrona precisa de apenas 23 linhas
  • Se, ao fazer poll de um future já concluído novamente, ele passar a retornar Poll::Pending em vez de panic, é possível cumprir o contrato sem comportamento unsafe, e em experimentos isso reduziu o tamanho do binário em 2% a 5% em firmware embarcado
  • Mesmo async { 5 }, sem await, hoje ainda gera uma máquina de estados com 3 estados básicos, mas otimizar isso para sempre retornar Poll::Ready(5) reduziu o tamanho de binários embarcados em 0,2%
  • O Project Goal proposto quer avançar no compilador com a remoção do panic após conclusão em modo release, eliminação da máquina de estados em blocos async sem await, inline de futures com um único await e fusão de estados idênticos

O problema de inchaço no nível do compilador em Async Rust

  • Async Rust permite executar código independente de executor ao mesmo tempo em servidores e microcontroladores, mas em microcontroladores pequenos o aumento no tamanho do binário é especialmente perceptível
  • O blog do Rust apresentou async/await como uma abstração de custo zero, mas na prática async gera bastante inchaço, e o mesmo problema também existe em desktop e servidor, só aparece menos por causa da abundância de memória e recursos de computação
  • Após as soluções alternativas para evitar inchaço ao escrever código async, foi submetido um Project Goal para resolver o problema no compilador
  • O problema de futures maiores do que o necessário e com cópias excessivas está fora do escopo

Estrutura do future gerado

  • O código de exemplo faz foo() retornar async { 5 }, e bar() executa foo().await + foo().await
  • Como bar tem 2 pontos de await, a máquina de estados precisa de pelo menos 2 estados, mas na prática vários estados extras são gerados
  • O compilador Rust pode despejar MIR em várias passagens, e a passagem coroutine_resume é a última passagem de MIR específica para async
    • Async ainda existe no MIR, mas não no LLVM IR, então a transformação de async em máquina de estados acontece na fase de MIR
  • A função bar gera 360 linhas de MIR, enquanto a versão síncrona usa apenas 23 linhas
  • O CoroutineLayout emitido pelo compilador é, na prática, um conjunto de estados em forma de enum
    • Unresumed: estado inicial
    • Returned: estado concluído
    • Panicked: estado após panic
    • Suspend0: primeiro ponto de await, armazenando o future de foo
    • Suspend1: segundo ponto de await, armazenando o primeiro resultado e o segundo future de foo
    Publicidade
  • Future::poll é uma função segura, então chamá-la novamente depois que o future já terminou não pode causar UB
    • Hoje, após Suspend1, ele retorna Ready e muda o future para o estado Returned
    • Se houver novo poll nesse estado, ocorre panic
  • O estado Panicked parece existir para impedir novo poll de um future depois que uma função async entrou em panic e isso foi capturado com catch_unwind
    • Depois de um panic, o future pode ficar em estado incompleto, então fazer poll novamente poderia levar a UB
    • Esse mecanismo é muito parecido com poisoning de mutex
    • Essa interpretação do estado Panicked tem cerca de 90% de confiança, já que é difícil encontrar documentação definitiva

É realmente necessário dar panic ao fazer poll após a conclusão?

  • Hoje, um future no estado Returned entra em panic, mas isso não é estritamente necessário
    • A única exigência é não causar UB
  • Panic é relativamente caro e adiciona um caminho com efeitos colaterais difíceis de remover por otimização
  • Se um future já concluído passar a retornar Poll::Pending quando receber novo poll, ainda será possível cumprir o contrato do tipo Future sem comportamento unsafe
  • Ao modificar o compilador para testar essa abordagem, foi observada uma redução de 2% a 5% no tamanho do binário em firmwares embarcados com async
  • Foi proposto oferecer esse comportamento como uma opção, de forma parecida com overflow-checks = false para overflow de inteiros
    • Em builds de debug, o panic continuaria para expor imediatamente o comportamento incorreto
    • Em builds de release, seria possível obter futures menores
  • Ao usar panic=abort, pode haver margem para remover o próprio estado Panicked, mas o impacto disso ainda precisa de análise adicional
Publicidade

Mesmo sem await, uma máquina de estados sempre é gerada

  • foo() retorna apenas async { 5 }, então a forma otimizada manualmente seria um future sem estado que sempre retorna Poll::Ready(5)
  • Porém, no MIR gerado pelo compilador, ainda existem os 3 estados básicos: Unresumed, Returned e Panicked
    • Ao fazer poll, o discriminante do estado atual é verificado e ocorre um desvio condicional
    • Se houver novo poll após a conclusão, acontece um panic com `async fn` resumed after completion
  • Nesse caso, seria possível otimizar para não criar máquina de estados e simplesmente retornar Poll::Ready(5) toda vez
  • Ao aplicar isso experimentalmente no compilador, o tamanho do binário embarcado caiu 0,2%
    • A redução não é grande, mas por ser uma otimização simples, talvez ainda valha a pena
  • Essa otimização muda ligeiramente o comportamento, mas só afeta executores que violam o contrato
    • Hoje o compilador entra em panic em polls posteriores
    • Após a otimização, o future sempre retorna Ready

Só o LLVM não basta

  • Mesmo que a saída de MIR seja ineficiente, em alguns casos o LLVM consegue limpar tudo, mas as condições são limitadas
    • O future precisa ser simples o suficiente
    • É preciso usar opt-level=3
  • Quando o future fica mais complexo, o LLVM não consegue eliminar tudo, e em código async idiomático em Rust os futures tendem a ser profundamente aninhados, então a complexidade cresce rápido
  • Em ambientes como embarcados ou wasm, onde otimização por tamanho é comum, o LLVM não consegue resolver isso sozinho
  • Exemplo no Godbolt: https://godbolt.org/z/58ahb3nne
    • No assembly gerado, o LLVM sabe que foo retorna 5, mas não consegue otimizar a resposta de bar para 10
    • A chamada da função poll de foo também permanece
    • O motivo são caminhos potenciais de panic que o compilador não consegue descartar completamente
    • O LLVM não sabe que foo na prática só será chamado uma vez e não entrará em panic
  • Se os ramos de panic forem comentados no IR, a otimização melhora: https://godbolt.org/z/38KqjsY8E
  • Em vez de esperar uma otimização posterior do LLVM, o compilador precisa fornecer uma entrada melhor para ele
Publicidade

O inline de futures não está funcionando bem

  • Inline é importante porque habilita passagens posteriores de otimização, mas os futures gerados em Rust hoje não são inlinados nessa fase inicial
  • Depois que cada future ganha sua implementação, LLVM e linker até têm chance de fazer inline, mas por causa dos problemas anteriores esse momento já é tarde demais
  • A oportunidade de inline mais direta é quando bar() apenas faz algo como foo(blah).await
    • Esse padrão aparece com frequência ao criar abstrações com traits
    • Hoje o compilador cria uma máquina de estados para bar e chama a máquina de estados de foo dentro dela
    • De forma mais eficiente, bar poderia ser o próprio future de foo
  • Quando há preâmbulo e pós-processamento, o caso fica mais complexo
    • Exemplo: bar(input) cria blah com input > 10, depois faz foo(blah).await e aplica * 2 ao resultado
    • Isso é comum ao transformar funções async para outra assinatura, especialmente em implementações de trait
  • Mesmo nessa forma, bar não precisa de estado async próprio
    • Não há dados preservados além do único ponto de await, exceto o valor capturado por foo
    • Ainda assim, bar não pode simplesmente se tornar o próprio foo, e pode depender da maior parte do estado de foo
  • Numa implementação manual, BarFut poderia ter os estados Unresumed { input } e Inlined { foo: FooFut }
    • No primeiro poll, ele executaria o preâmbulo, criaria foo(blah) e mudaria para Inlined
    • Depois disso, aplicaria o pós-processamento ao resultado de foo.poll(cx)
  • Se fosse possível executar código antecipadamente até o primeiro ponto de await, também daria para remover o estado Unresumed, mas isso não pode mudar porque há a garantia de que o future não faz nada antes de receber poll
  • Se fosse possível consultar propriedades do future durante o poll, mais otimizações de inline poderiam acontecer
    • Por exemplo, se fosse sabido que o future sempre retorna ready no primeiro poll, o future chamador não precisaria criar estado para aquele ponto de await
    • Aplicar esse tipo de otimização recursivamente permitiria fundir muitos futures em máquinas de estados bem mais simples
  • Pela estrutura atual do rustc, cada bloco async parece ser transformado isoladamente e os dados relevantes não são preservados depois, o que impede esse tipo de consulta
  • O inline de futures ainda não foi testado experimentalmente, mas há expectativa de grande benefício para tamanho de binário e desempenho
Publicidade

Fusão de estados idênticos

  • Cada ponto de await em um bloco async adiciona um estado extra à máquina de estados
  • O código abaixo é natural, mas como os dois ramos fazem await da mesma função async, surgem 2 estados idênticos
    • CommandId::A => send_response(123).await
    • CommandId::B => send_response(456).await
  • Nesse caso, o CoroutineLayout passa a ter _s0 e _s1, ambos armazenando o mesmo tipo de coroutine de send_response, além dos estados Suspend0 e Suspend1
  • O MIR dessa função tem 456 linhas, e muitos blocos básicos são essencialmente duplicados
  • Se o código for refatorado manualmente para primeiro calcular apenas o valor da resposta e depois chamar uma única vez send_response(response).await, os estados duplicados desaparecem
    • CommandId::A vira 123
    • CommandId::B vira 456
    • Depois disso, send_response(response).await
  • Após a refatoração, o CoroutineLayout passa a ter apenas um future armazenado e sobra só o estado Suspend0
  • O comprimento total do MIR cai para 302 linhas, eliminando a duplicação
  • Portanto, parece útil ter uma passagem de otimização que detecte caminhos de código e estados equivalentes para fundi-los
    • Essa otimização provavelmente combina bem com uma passagem de inline de futures

Links dos experimentos e benchmarks adicionais

Pedido de apoio ao Project Goal

2 comentários

 
GN⁺ 2026-05-06
Comentários no Hacker News
  • Concordo que o título é um pouco exagerado, mas o texto foi bem escrito e transmite bem a ideia
    Ainda não tenho experiência suficiente com async em Rust para ter uma opinião forte, mas algumas coisas me chamaram a atenção
    O lado bom é que dá para ter um runtime explícito. Em vez de contaminar o projeto inteiro com async, dá para manter o padrão como síncrono e usar o runtime só nas “bordas” de E/S
    Isso funcionou bem no projeto em que estou trabalhando, e parece bem parecido com a estratégia que o Zig adota para código de E/S. Nesse caso, o problema da cor das funções também ficou em grande parte resolvido, e como era preciso separar com rigor o código de E/S do código centrado em CPU, um runtime explícito de E/S pareceu natural
    O lado ruim é que o ecossistema inteiro parece depender demais de tokio. É como se o GC do Java fosse opcional, mas na prática todo mundo usasse o mesmo runtime de GC de terceiros, e qualquer biblioteca que você importasse te obrigasse a usar esse runtime. Esse tipo de dependência central não é saudável

    • Dependendo do contexto, pode realmente parecer que o ecossistema inteiro depende de tokio, mas olhando para Rust embarcado isso faz mais sentido
      Os requisitos de runtime async em processadores de workstation e em ambientes como o RP2040 são muito diferentes. Ainda assim, como é possível trocar o backend, ao escrever código async de E/S para um microcontrolador ARM M0 pequeno, usando o embassy, que é um runtime voltado para embarcados, o código acaba parecendo quase igual ao usado em outros ambientes
      Como usa os mesmos traits e interfaces, dá para se preocupar menos com os detalhes do runtime. Comparado a usar um RTOS pequeno ou montar o próprio ambiente async, isso é muito bom
      O que aprendi escrevendo código async com embassy também pode ser levado para outras áreas
    • Fico curioso sobre qual seria a alternativa. Estou satisfeito usando tokio, mas também acho bom que outras pessoas usem outros executores como smol, async-std e glommio
      Mesmo não fazendo parte da biblioteca padrão, tokio é bem mantido, então a situação atual me parece aceitável. Na verdade, se entrasse na biblioteca padrão, eu até me preocuparia que isso dificultasse o uso de outros executores e tornasse mais difícil portar a biblioteca padrão para outras plataformas
      Claro, pode ser que essa preocupação não tenha fundamento
    • É interessante você mencionar Java, porque Java historicamente também passou por problemas parecidos
      Logging hoje está mais organizado em torno de slf4j, mas ainda há bibliotecas que usam outras coisas, e utilitários comuns começaram com Apache Commons, enquanto hoje muita coisa usa Guava
      JSON se organizou em parte em torno do Jackson, mas Gson e Simple-json ainda são comuns, e as anotações de nulidade também nunca foram oficializadas: passaram de distribuições não oficiais do JSR-305 para o checker framework e mais recentemente estão migrando para o JSpecify
      Esses elementos básicos precisam ser fornecidos pela linguagem para evitar fragmentação e a proliferação de bibliotecas que viram padrões de fato
    • Há muitas áreas em que dá para usar Rust com async sem depender de tokio. Na prática, quem parece realmente preso a tokio é mais o lado de web/servidor
      Escrever bibliotecas independentes de executor não é tão difícil, mas exige atenção constante, e isso nem sempre é seguido pela maior parte da comunidade
  • Excelente texto. Gosto desse tipo de análise profunda de otimização, e espero que as metas do projeto avancem bem
    Já tive a impressão de que o compilador muitas vezes não investe tanto esforço em otimizar casos “triviais”
    Dito isso, o título é dramático demais para o conteúdo. Eu teria clicado mesmo se fosse “Async Rust Optimizations the Compiler Still Misses”

    • O título foi escolhido assim simplesmente porque é verdade. Desde que async entrou, por volta de 2019, pouca coisa mudou de forma significativa
      Hoje já dá para usar async em traits e closures, mas isso é uma atualização do sistema de tipos, não uma mudança na mecânica do async em si. O Waker também ficou um pouco mais fácil de lidar, mas isso é mais uma melhoria de std/core
      Pelo que entendo, as pessoas que fizeram o landing do async Rust acabaram sofrendo bastante burnout e reduziram a participação, e quase ninguém assumiu depois. Ainda assim, é bem animador ver que o pessoal do Google abriu um PR para otimizar o layout de memória das variáveis capturadas
      Eu e meus colegas usamos bastante async, então talvez a gente mesmo precise fazer isso, ou pelo menos começar. Parece aquele tipo de “grátis” que é grátis no mesmo sentido de ganhar um filhote
      Então sim, o título é um pouco caça-clique, mas mesmo assim não pretendo voltar atrás nele
    • Concordo que o título é exagerado demais
      O autor parece obcecado com o overhead de funções triviais. Ele se incomoda com o overhead dos estados “panic” e “returned”, mas isso não é um grande problema
      A maioria dos blocos async úteis é grande o bastante para que o overhead de casos de erro se dilua
      Pode haver um ponto válido sobre falta de inlining. Mas, em geral, o que limita grandes quantidades de atividades é o espaço de estado exigido por cada uma delas
  • Async no geral parece uma ideia pouco madura. Código comum já era assíncrono
    Se você precisa esperar uma tarefa async, a thread dorme até ficar pronta, e o kernel abstrai isso para você. Só que as pessoas não gostaram de estruturar o código em torno de threads lógicas, então adicionaram um sistema de callbacks para eventos, e depois perceberam que callbacks são difíceis de raciocinar e que controle sequencial é melhor
    Então eu diria que threads são o modelo de programação correto
    Agora os runtimes de linguagem preferem “green threads” por motivos de portabilidade e desempenho, mas a maioria das linguagens não oferece isso direito. Em vez disso, surgem problemas como a cor async/non-async, escalonamento, prioridades e ausência de preempção. É um modelo de processos e escalonamento pior do que nos anos 1970

    • A afirmação de que “código comum já era async, e quando espera a thread dorme e o kernel abstrai” não está correta
      Mesmo código async muitas vezes é escrito de forma que não maximiza a concorrência expressável. Por exemplo, em vez de “execute N tarefas de E/S todas ao mesmo tempo”, escreve-se algo como “para cada tarefa X, await process(x)
      Mas no mundo das threads esse problema de concorrência é ainda pior. Threads são pesadas demais por natureza para expressar concorrência de forma eficiente, e não há como otimizá-las nessa direção
      Isso não é uma lição nova. Executores com work stealing já eram conhecidos há muito tempo por terem latência muito menor e P99 mais consistente do que threads tradicionais. Foi por isso que a Apple criou o GCD no começo dos anos 2000
      Threads não fornecem ao escalonador do kernel informações mais ricas de que ele precisa para entender a carga de trabalho, e threads de kernel são um mecanismo pesado demais para conseguir concorrência fina. Em cargas de E/S ou mistas isso é ainda pior
      Nem todo programa precisa desse nível de desempenho, mas é muito mais fácil atingir um patamar mais alto de performance com o mesmo esforço, e de fato dá para alcançar latência e throughput que abordagens tradicionais têm dificuldade de acompanhar
      Um sinal de que async está indo na direção certa também aparece em io_uring. A abordagem de E/S de alto desempenho do kernel com io_uring é completamente diferente do modelo tradicional de threads e syscalls, e o tratamento de conclusão é muito mais próximo da concorrência async. O problema é que async/await sozinho não tem cores suficientes para expressar as relações entre tarefas async, então é mais difícil explorar isso por completo
    • No momento em que o kernel e o escalonador do SO entram no caminho, você pode ficar 3 a 4 ordens de magnitude mais lento do que deveria
      Da última vez que mexi com código de corrotinas/escalonamento, criar uma thread que terminava imediatamente e dar join nela levava cerca de 200µs, enquanto criar, escalonar e esperar uma green thread própria levava cerca de 400ns
      Não é preciso esperar 10 anos até alguém projetar outro framework async absurdamente complexo. Em qualquer linguagem de sistemas, com umas 20 linhas de assembly dá para fazer sua própria green thread/corrotina com stack
    • Se threads são o modelo de programação correto depende do que você está fazendo. Para trabalho centrado em computação, threads fazem sentido; para trabalho centrado em largura de banda, async faz sentido
      Otimizar código centrado em largura de banda é um problema de desenho de escalonamento. No modelo clássico multithread, você tem controle limitado sobre o escalonamento; no modelo async, você consegue controlar quase perfeitamente
      Um agendamento async bem otimizado é muito mais rápido que uma arquitetura multithread equivalente na mesma carga centrada em largura de banda, a ponto de nem haver comparação
      Hoje, grande parte do código de alto desempenho é centrado em largura de banda, e async existe para tornar mais fácil otimizar esse tipo de carga
    • Eu acho que callbacks, na verdade, são mais fáceis de raciocinar
      Ao testar processamento concorrente e verificar se lida corretamente com condições de corrida, callbacks tornam isso muito mais fácil porque você pode controlar o escalonamento. Como cada callback representa uma unidade separada, dá para ver quais eventos podem ser reordenados e analisar mais facilmente ordens diferentes
      Já com threads, é fácil ignorar a ordem e deixar de pensar em quando a complexidade de outras threads pode afetar a thread atual. Não é exatamente simplicidade; é mais uma simplificação
      Além disso, é difícil testar de fato cenários concorrentes alterando a ordem real das coisas, a menos que você introduza barreiras artificiais para parar threads, ou troque a E/S por stubs e passe mocks com callbacks que controlem a ordem
      O problema dos callbacks é que a pilha de chamadas capturada não é a pilha lógica de chamadas. Sem bibliotecas/runtimes que trabalhem para tornar a pilha significativa, é preciso uma boa definição de erro
      Claro, também é possível misturar os dois paradigmas e ficar só com as desvantagens de ambos
    • Threads não são melhores nem piores que async+callbacks; são modelos diferentes. Existem problemas que combinam bem com threads, e outros que são muito melhor expressos com async
  • Se o objetivo principal do Rust é segurança, eu não entendo por que existe panic. Deveria ser possível provar que não há nenhum caminho no código capaz de causar panic
    Passei a semana inteira olhando isso, e é muito difícil fazer um programa com garantia de que nunca dará panic. Pelo que entendi, o handler de panic ocupa algo como 300KB, e a única forma de excluí-lo é fazer com que no momento da compilação não exista absolutamente nenhum caminho no código que possa causar panic. Verificar depois da compilação se o binário incluiu um handler de panic parece um hack
    Dá para proibir unwrap e outras operações que causam panic com lint, mas se existisse um subconjunto no-panic de Rust, boa parte dos problemas tratados neste texto desapareceria
    É frustrante lidar com uma linguagem em que há tantas operações que teoricamente podem causar panic, mesmo em situações que na prática só aconteceriam com algo no nível de bit flip. Isso vale tanto para provar que um array não está vazio quanto para lidar com async
    No fim, você acaba enchendo o código de tratamento de erro para situações que nunca vão acontecer, ou usando estruturas estranhas como o padrão de lista não vazia com primeiro campo e resto da lista separados. E mesmo essa estrutura adiciona seu próprio inchaço

    • O pessoal do Rust-in-Linux está lidando com isso com coisas como operações de memória passíveis de falha. Para eles, isso é uma funcionalidade necessária
      O trabalho para ampliar usos baseados em prova, incluindo provas de que um array não está vazio, também está avançando aos poucos
    • Panic é bem importante para usabilidade e segurança
      Se não existisse panic e fosse preciso continuar executando em qualquer situação, então em casos como corrupção de memória que quebra invariantes você teria de colocar muito tratamento de erro em todo lugar que verifica invariantes para tentar se recuperar
      Isso seria exatamente o mesmo tipo de problema que você está apontando: uma quantidade enorme de tratamento de erro para situações que quase nunca vão acontecer
    • O objetivo do Rust é segurança de memória. Panic é totalmente seguro do ponto de vista de segurança de memória
    • Nem mesmo o SO que executa o programa é perfeito
      Cansa ver essa postura de querer que as ferramentas tornem tudo infalível sem querer fazer nada por conta própria. Quer-se uma API fácil; se não for fácil o bastante, quer-se “programar” contêineres Kubernetes com YAML; se isso também não for fácil o bastante, quer-se um serviço de hospedagem com cliques da GCP ou da Amazon
      No fim, parece menos programação e mais desejo de consumir apps infalíveis, e esse modo de vida só existe em relação simbiótica com quem constrói as coisas
  • Esse tipo de discussão feia, mas necessária já acontece no C++ há algum tempo
    Desde que async foi introduzido em Rust eu não gostava da sua natureza contagiosa
    Quero que Rust dê certo, e se mais gente assim aparecer, o futuro do Rust pode ficar mais promissor

  • Comecei recentemente a trabalhar com async em Rust, e o principal problema que estou enfrentando agora é duplicação de código
    Toda função que eu quero que suporte tanto uma API assíncrona quanto uma API bloqueante precisa ser escrita em duplicidade. Seria ótimo ter maybe-async
    Tentei contornar isso olhando crates como maybe-async e bisync, mas todos tinham problemas ou restrições fortes

    • Há um trabalho em andamento de genéricos de palavra-chave para permitir tornar funções genéricas em relação a palavras-chave como async ou const
      No momento, a melhor opção para escrever código que queira viver tanto no mundo síncrono quanto no assíncrono é sans-io. Thomas Eizinger, do Fireguard, escreveu um bom texto sobre esse padrão[1]
      Esse padrão não só resolve de forma elegante a questão sync/async, como também facilita testes e abre caminho para técnicas como DST[2]
      Também escrevi um texto sobre isso[3], destacando que o problema vai além de async versus sync e inclui a questão mais ampla de diferentes executores
      0: https://github.com/rust-lang/effects-initiative
      1: https://www.firezone.dev/blog/sans-io
      2: https://notes.eatonphil.com/2024-08-20-deterministic-simulat...
      3: https://hugotunius.se/2024/03/08/on-async-rust.html
    • Depende muito do que você está fazendo na prática, mas se for simples o bastante, talvez dê para criar um macro que troque tipos e awaits
    • É o clássico problema da cor das funções. https://journal.stuffwithstuff.com/2015/02/01/what-color-is-...
    • Do meu ponto de vista, uma função async já é maybe-async
      A diferença entre fn -> void e fn -> Future é que a primeira termina imediatamente, enquanto a segunda pode terminar só depois
      Se você quer executar uma função async de forma bloqueante, basta usar um executor bloqueante
  • Gostei deste texto porque ele até me fez olhar para as metas do Rust para 2026
    Meu time usa Rust, mas não tivemos necessidade de mergulhar tão fundo para fazer o que precisávamos. Mesmo assim, é divertido ver uma linguagem evoluindo desde a base com tanto feedback da comunidade
    No C++ eu não tinha muito essa sensação, e também não sei bem como isso funciona em outras áreas
    Só acho uma pena que cada meta pareça precisar de financiamento específico, o que dá um ar meio Kickstarter. Fico curioso se esse é o melhor modelo que encontraram até agora

    • O termo “metas de projeto” é bastante enganoso em relação ao que realmente significa
      Metas de projeto são um sistema em que uma pessoa ou um pequeno grupo expressa que quer trabalhar em algo e pede aos voluntários do projeto Rust um compromisso contínuo de suporte, como revisão de código ou resposta a perguntas
      Isso não significa que o projeto Rust em si definiu essa meta, nem que necessariamente a apoia
      Então não é correto ver isso como um roadmap oficial do Rust; é mais preciso entender como “há contribuidores interessados em trabalhar nesta área”
    • Parece haver um consenso até dentro do comitê ISO do C++ de que o processo de evolução da linguagem está, em certa medida, quebrado. Principalmente por causa da escala e da forma de organização
      Infelizmente, quando uma tecnologia se estabelece comercialmente, parece que esse tipo de coisa acaba acontecendo. Também é difícil culpar grandes patrocinadores por financiarem apenas as partes que lhes interessam
      Felizmente, pelo que sei, uma parte significativa do financiamento da TweedeGolf vem do governo holandês
    • Parece haver dois grandes tipos de trabalho em open source: desenvolvimento de funcionalidades e manutenção
      Funcionalidades novas são algo que dá para “vender”. Custam dinheiro para serem construídas, mas resolvem problemas reais, e se o custo desse problema for maior que o custo de desenvolver a funcionalidade, empresas em geral aceitam pagar
      Manutenção é mais difícil, mas hoje já existem fundos para mantenedores. Um exemplo é o fundo da RustNL: https://rustnl.org/maintainers/
      Esses fundos se destinam a trabalhos mais amplos e contínuos, sustentados por várias organizações contribuindo um pouco cada uma
      Não sei se é o melhor modelo, mas pelo menos parece funcionar até certo ponto
  • Se você ler a documentação de Rust Async e Tokio, está bem explicado por que não se deve colocar partes intensivas em CPU na stack async, como usar de forma eficiente ferramentas básicas como std::sync::Mutex dentro de blocos async, e como ligar código síncrono a código async
    Muito código não segue essas orientações porque não se importa com eficiência, ou não precisa dela. Mas há muitos projetos que valorizam desempenho e eficiência, e quando o código vai para produção as armadilhas ficam evidentes. ScyllaDB é um exemplo
    LLM também não ajuda. Faz tudo virar async até main, usa as ferramentas básicas erradas e não desenha o sistema direito

  • A fusão de estados duplicados, ou seja, o padrão de puxar o match para fora dos ramos com await, como no exemplo process_command, é provavelmente a coisa mais fácil que qualquer um pode aplicar hoje em código async existente
    Não exige trabalho do compilador, só refatoração

    • No mínimo, seria preciso um lint customizado para encontrar onde isso pode ser aplicado. Isso já fica bem perto de trabalho de compilador
  • Sobre a parte de que “Futures não são facilmente inlineadas”, na linguagem de programação que eu criei escrevi um passe customizado para fazer inline de chamadas de funções async dentro de funções async
    Em geral funciona bem e consegue eliminar parte do boilerplate, mas o tamanho do binário resultante aumenta bastante
    Tecnicamente, Rust também poderia fazer a mesma coisa

 
GN⁺ 2026-05-06
Opiniões no Lobste.rs
  • Foi um texto muito mais construtivo do que eu esperava só pelo título

    • Acho que está bem próximo dos fatos. Já se passaram 7 anos desde o lançamento do MVP, mas quase não houve avanço no design da linguagem nem na implementação do compilador, e as pessoas que em grande parte criaram o MVP reduziram sua atuação no projeto mais ou menos na mesma época, então o repasse adiante acabou parando
      Espero que quem queira trabalhar nisso receba o apoio necessário
  • I want to work on this in the compiler and as such have submitted it as a Project Goal

    Stop generating statemachines that don’t have to be there
    Make the compiler’s job easier by removing panic paths and branches
    Make statemachines smaller

    É bom ver esse problema sendo tratado. Já vi algumas vezes gente dizendo que hoje o rustc passa código demais para o LLVM e espera que o otimizador resolva tudo, e este texto em especial também está pedindo financiamento para esse trabalho

  • Meu Deus, eu fui burro
    Sempre pensei que async fosse inerentemente “inchado”, já que de qualquer forma precisa de runtime, rastreamento de tarefas e polling para verificar conclusão. Então esse overhead nunca é zero
    Eu entendia que a “abstração sem custo” aqui era sobre o recurso da linguagem, separada do runtime adicional
    Nunca nem me ocorreu olhar o que o rustc emite antes de passar para o LLVM

  • Para quem não está familiarizado com async Rust:

    It's amazing how we can write executor agnostic code that can run concurrently on huge servers and tiny microcontrollers.

    Isso é realmente verdade. Até uma árvore de chamadas async aninhadas, quando recebe otimização máxima, acaba consolidada em uma única struct com máquinas de estado dentro. É uma abordagem realmente engenhosa

  • Em build de release, chegar a esse caso gera algum tipo de deadlock? Ou pode haver vazamento por causa de tarefas esperando trabalhos que ficam sempre em Pending?

    • Sim. Esses futures ficam em um estado travado e nunca terminam. Mas esse estado só pode ser alcançado por código async de baixo nível que já esteja com bug, e código que não consegue rastrear corretamente um future concluído provavelmente já está causando vazamentos e deadlocks
      Não dá para fazer polling incorreto usando .await
  • Algumas ideias me ocorreram:

    1. Este texto parece defender que mais lógica de otimização seja tirada de dentro do LLVM e movida para a camada MIR. Por exemplo, entendo por que o inlining de funções async seria mais fácil no MIR do que no LLVM. Se isso foi possível no MIR para async, fico pensando se daria para generalizar essa lógica também para funções síncronas e remover alguns passes de otimização do LLVM. Sei que é um trabalho grande, e isso é mais uma direção do que uma pergunta prática. Quando o compilador de frontend/middle-end atinge certo nível de complexidade, talvez faça mais sentido deslocar uma parte considerável das otimizações genéricas do LLVM para outro lugar
    2. Ainda não gosto de panic=unwind. Tirando alguns test harnesses, quase nunca vi vantagens sobre panic=abort que compensem o custo. Mesmo para test harnesses, no Linux parece que daria para aplicar uma escolha parecida, usando clone de forma meio obscura para fazer wait na thread executora em vez de pthread_join. Posso estar errado nessa parte
  • O link morreu agora há pouco para mais alguém?
    Edit: o post do blog aparece por meio segundo e depois vai para uma página 404
    Edit 2: entrei na lista de posts do blog, cliquei em várias coisas, e até abrindo aquele post a partir da lista vai para uma página 404. Como alguém consegue quebrar assim um blog estático, ou pelo menos que deveria ser estático?

    • O tom parece um pouco desnecessariamente rude e agressivo. Sites também podem ter bugs, e reportar isso é útil, mas este comentário soa um pouco mal-humorado
      Para constar, acho que segui os mesmos passos de reprodução e não vi nenhum 404. Testei no celular e no desktop, com JavaScript ligado e desligado. Então o que você encontrou talvez fosse mais complexo do que parecia