Async Rust nunca salió del estado MVP
(tweedegolf.nl)- Async Rust permite ejecutar código independiente del executor tanto en servidores como en microcontroladores, pero la máquina de estados que genera el compilador hace que el aumento del tamaño del binario se note especialmente en embebidos
- Incluso un ejemplo simple como
bar(), con 2 puntos de await, crea 360 líneas de MIR y los estadosUnresumed,Returned,Panicked,Suspend0,Suspend1; la versión síncrona solo necesita 23 líneas - Si al volver a hacer poll de un future ya completado se devuelve
Poll::Pendingen vez depanic, se puede cumplir el contrato sin comportamiento unsafe, y en pruebas el tamaño del binario se reduce entre 2% y 5% en firmware embebido - Incluso
async { 5 }, sin await, hoy genera una máquina de estados con 3 estados base, pero si se optimiza para devolver siemprePoll::Ready(5), el tamaño del binario embebido baja 0.2% - El Project Goal propuesto busca que el compilador impulse en modo release la eliminación del panic tras completarse, la eliminación de la máquina de estados en bloques async sin await, el inline de futures con un solo await y el plegado de estados idénticos
El problema del bloat a nivel de compilador en Async Rust
- Async Rust permite ejecutar código independiente del executor tanto en servidores como en microcontroladores, pero en microcontroladores pequeños el aumento del tamaño del binario se nota especialmente
- El blog de Rust presentó async/await como una abstracción de costo cero, pero en la práctica async sí genera bastante bloat; el mismo problema existe en desktop y servidores, aunque allí se nota menos por tener más memoria y capacidad de cómputo
- Tras un método alternativo para evitar el bloat al escribir código async, se presentó un Project Goal para resolver el problema desde el compilador
- Queda fuera del alcance el problema de futures más grandes de lo necesario y con demasiadas copias
- Este problema ya es conocido y hay un PR abierto que aborda una parte: https://github.com/rust-lang/rust/pull/135527
Estructura del future generado
- El código de ejemplo hace que
foo()devuelvaasync { 5 }, y quebar()ejecutefoo().await + foo().await- Ejemplo en Godbolt: godbolt
bartiene 2 puntos de await, así que la máquina de estados necesita al menos 2 estados, pero en la práctica se generan más- El compilador de Rust puede volcar MIR en varias pasadas, y la pasada
coroutine_resumees la última pasada de MIR específica de async- async aún existe en MIR, pero ya no en LLVM IR, así que la conversión de async a máquina de estados ocurre en las pasadas de MIR
- La función
bargenera 360 líneas de MIR, mientras que la versión síncrona solo usa 23 líneas - El
CoroutineLayoutque emite el compilador es, en la práctica, un conjunto de estados con forma de enumUnresumed: estado inicialReturned: estado completadoPanicked: estado posterior a un pánicoSuspend0: primer punto de await, guarda el future defooSuspend1: segundo punto de await, guarda el primer resultado y el segundo future defoo
Future::polles una función segura, así que no debe provocar UB aunque se vuelva a llamar después de que el future ya terminó- Hoy, después de
Suspend1, devuelveReadyy cambia el future al estadoReturned - Si se vuelve a hacer poll en ese estado, ocurre un panic
- Hoy, después de
- El estado
Panickedparece existir para impedir que se vuelva a hacer poll de ese future cuando una función async hizo panic y eso fue capturado concatch_unwind- Tras un panic, el future puede quedar en un estado incompleto, así que volver a hacer poll podría llevar a UB
- Este mecanismo es muy similar al poisoning de un mutex
- Esta interpretación del estado
Panickedno está totalmente documentada; la certeza es de alrededor del 90%
¿De verdad tiene que hacer panic al hacer poll después de completarse?
- Hoy un future en estado
Returnedhace panic, pero no es estrictamente necesario que sea así- La condición necesaria es solo que no cause UB
- Un panic es relativamente costoso y añade una ruta con efectos secundarios que cuesta eliminar por optimización
- Si al volver a hacer poll de un future ya completado se devuelve
Poll::Pending, se puede cumplir el contrato del tipoFuturesin comportamiento unsafe - Al modificar el compilador para probar este enfoque, se observó una reducción de 2% a 5% del tamaño del binario en firmware embebido async
- Se propone ofrecer este comportamiento como un switch, de forma similar a
overflow-checks = falsepara overflow de enteros- En builds de debug seguiría habiendo panic para exponer de inmediato el comportamiento incorrecto
- En builds release se podrían obtener futures más pequeños
- Con
panic=abort, podría ser posible eliminar por completo el estadoPanicked, aunque su impacto requiere más revisión
Siempre se genera una máquina de estados, incluso sin await
foo()solo devuelveasync { 5 }, así que la forma óptima manual sería un future sin estado que siempre devuelvaPoll::Ready(5)- Sin embargo, en el MIR que genera el compilador siguen existiendo los 3 estados base:
Unresumed,Returned,Panicked- Al hacer poll, se revisa el discriminante del estado actual y se bifurca en consecuencia
- Si se vuelve a hacer poll tras completarse, hay un panic con el assert
`async fn` resumed after completion
- En este caso se puede optimizar para no crear ninguna máquina de estados y devolver siempre
Poll::Ready(5) - Al aplicar esto experimentalmente en el compilador, el tamaño del binario embebido se redujo 0.2%
- El ahorro no es grande, pero la optimización es simple y podría valer la pena
- Esta optimización cambia un poco el comportamiento, pero solo afecta a executors que no respetan el contrato
- Hoy el compilador hace panic en polls posteriores
- Tras la optimización, el future siempre devolvería
Ready
Solo con LLVM no alcanza
- Aunque la salida MIR sea ineficiente, a veces LLVM puede limpiar todo, pero bajo condiciones limitadas
- El future debe ser lo suficientemente simple
- Hay que usar
opt-level=3
- Si el future se vuelve más complejo, LLVM ya no logra eliminarlo, y en código async idiomático de Rust los futures tienden a anidarse profundamente, así que la complejidad crece rápido
- En entornos donde a menudo se optimiza por tamaño, como embebidos o wasm, LLVM no logra optimizar todo esto por sí solo
- Ejemplo en Godbolt: https://godbolt.org/z/58ahb3nne
- En el ensamblador generado, LLVM sabe que
foodevuelve 5, pero no logra optimizar la respuesta debara 10 - La llamada a la función
polldefoosigue presente - La razón son rutas potenciales de panic que el compilador no puede descartar completamente
- LLVM no sabe que
fooen realidad solo se llama una vez y que no va a hacer panic
- En el ensamblador generado, LLVM sabe que
- Si se comenta la rama de panic en el IR, la optimización mejora: https://godbolt.org/z/38KqjsY8E
- En vez de esperar una optimización posterior de LLVM, el compilador debería darle una mejor entrada a LLVM
El inline de futures no está funcionando bien
- El inline es importante porque habilita pasadas de optimización posteriores, pero los futures generados por Rust hoy no se inlinean en etapas tempranas
- Una vez que cada future obtiene su implementación, LLVM y el linker sí tienen oportunidades de inline, pero por los problemas anteriores ese momento ya llega demasiado tarde
- La oportunidad de inline más directa es una forma donde
bar()solo hacefoo(blah).await- Es un patrón que aparece seguido al construir abstracciones con traits
- Hoy el compilador crea una máquina de estados para
bary dentro de ella llama a la máquina de estados defoo - De manera más eficiente,
barpodría ser directamente el future defoo
- Cuando hay preamble y postamble, la cosa es más compleja
- Ejemplo:
bar(input)creablahconinput > 10, luego hacefoo(blah).awaity finalmente aplica* 2al resultado - Es algo común al transformar funciones async a otras firmas, especialmente en implementaciones de traits
- Ejemplo:
- Incluso en esta forma,
barno necesita tener estado async propio- No hay datos preservados más allá del único punto de await excepto el valor capturado por
foo - Aun así,
barno puede convertirse simplemente enfoo, sino que la mayor parte del estado puede delegarse afoo
- No hay datos preservados más allá del único punto de await excepto el valor capturado por
- En una implementación manual,
BarFutpodría tener los estadosUnresumed { input }eInlined { foo: FooFut }- En el primer poll ejecutaría el preamble para crear
foo(blah)y cambiar al estadoInlined - Después aplicaría el postamble al resultado de
foo.poll(cx)
- En el primer poll ejecutaría el preamble para crear
- Si el código pudiera ejecutarse por adelantado hasta el primer punto de await, también podría eliminarse el estado
Unresumed, pero no puede hacerse porque está garantizado que un future no hace nada antes de recibir poll - Si se pudieran consultar propiedades del future mientras está siendo polleado, sería posible aplicar optimizaciones de inline adicionales
- Por ejemplo, si se supiera que un future siempre devuelve ready en su primer poll, el future llamador no necesitaría crear un estado para ese punto de await
- Si este tipo de optimización se aplicara recursivamente, muchos futures podrían plegarse en máquinas de estados mucho más simples
- En la estructura actual de
rustc, cada bloque async parece transformarse por separado y luego no se conserva la información relacionada, así que este tipo de consulta no parece posible - El inline de futures aún no se ha probado, pero se espera que ayude bastante tanto en tamaño de binario como en rendimiento
Plegado de estados idénticos
- Cada punto de await dentro de un bloque async añade un estado extra a la máquina de estados
- Un código como el siguiente es natural, pero como ambas ramas hacen await de la misma función async, se generan 2 estados idénticos
CommandId::A => send_response(123).awaitCommandId::B => send_response(456).await
- En este caso, el
CoroutineLayoutcrea_s0y_s1, ambos almacenando el mismo tipo de coroutine desend_response, además de los estadosSuspend0ySuspend1 - El MIR de esta función tiene 456 líneas, y muchos bloques básicos son en la práctica duplicados
- Si antes se refactoriza manualmente el código para calcular primero solo el valor de respuesta y luego hacer una sola vez
send_response(response).await, desaparecen los estados duplicadosCommandId::Aproduce123CommandId::Bproduce456- Después se hace
send_response(response).await
- Tras el refactor, en
CoroutineLayoutqueda almacenado un solo future y solo queda el estadoSuspend0 - La longitud total del MIR baja a 302 líneas y desaparece la duplicación
- Por eso parece útil una pasada de optimización que detecte rutas de código y estados equivalentes para plegarlos en uno solo
- Esta optimización probablemente combine bien con una pasada de inline de futures
Enlaces de experimentos y benchmarks adicionales
- Al aplicar juntos los 2 experimentos, se obtiene alrededor de 3% de mejora de rendimiento en un benchmark sintético x86 usando el executor
smol - No panics in poll after ready: https://github.com/rust-lang/rust/compare/main...diondokter:rust:resume-pending
- No await, no statemachine: https://github.com/rust-lang/rust/compare/main...diondokter:rust:no-statemachine-when-no-await
Solicitud de apoyo para el Project Goal
- Este trabajo fue presentado como Project Goal para poder llevarse adelante en el compilador: https://rust-lang.github.io/rust-project-goals/2026/async-statemachine-optimisation.html
- Sin financiamiento es difícil avanzar mucho, así que se necesita apoyo parcial o total de empresas u organizaciones que se beneficien de este trabajo
- El contacto es
dion@tweedegolf.com - El alcance del trabajo y el monto de financiamiento necesario son flexibles, pero se estima que €30k permitirían completar todo o una parte importante
2 comentarios
Comentarios de Hacker News
Estoy de acuerdo en que el título es un poco exagerado, pero el texto está bien escrito y transmite bien la idea principal
Aún no tengo tanta experiencia con Rust async como para tener opiniones muy firmes, pero sí hubo varias cosas que me llamaron la atención
Lo bueno es que puedes tener un runtime explícito. En vez de contaminar todo el proyecto con async, puedes dejar lo síncrono como base y usar el runtime solo en los “límites” de entrada/salida
En un proyecto en el que estoy trabajando, este enfoque encajó bien, y se ve bastante parecido a la estrategia que Zig adopta para el código de entrada/salida. En este caso, también resolvió en gran parte el problema del color de las funciones, y como necesitábamos separar estrictamente el código de entrada/salida del código orientado a CPU, un runtime explícito de I/O se sentía natural
Lo malo es que todo el ecosistema parece depender demasiado de tokio. Es parecido a si en Java el GC fuera opcional, pero en la práctica todos usaran el mismo runtime de GC de terceros, y cualquier librería que importaras te lo impusiera. Ese tipo de dependencia central no es saludable
Los requisitos de un runtime async en procesadores de estación de trabajo y en entornos como un RP2040 son muy distintos. Aun así, como puedes cambiar el backend, incluso cuando escribes código async de entrada/salida para un microcontrolador ARM M0 pequeño, si usas embassy, que es un runtime orientado a embebidos, el código se ve casi igual al que usarías en otros entornos
Como se usan los mismos trait e interfaces, puedes preocuparte menos por los detalles del runtime. Comparado con usar un RTOS pequeño o construir tú mismo el entorno async, está bastante bien
Lo que aprendes usando código async con embassy también lo puedes llevar a otras áreas
Aunque tokio no sea parte de la biblioteca estándar, está bien mantenido, así que la situación actual me parece aceptable. De hecho, me preocupa que si entrara a la biblioteca estándar sería más difícil usar otros ejecutores, y también sería más difícil portar la biblioteca estándar a otras plataformas
Claro, puede que esta preocupación no tenga fundamento
El logging hoy más o menos se ordenó con slf4j, pero todavía hay librerías que usan otras cosas, y las utilidades comunes empezaron con Apache Commons y ahora muchas usan Guava
JSON se ordenó en cierta medida con Jackson, pero Gson y Simple-json siguen siendo comunes, y las anotaciones de nulabilidad tampoco lograron oficializarse: pasaron de distribuciones no oficiales del fallido JSR-305, luego por checker framework, y más recientemente hacia JSpecify
Estos elementos básicos debería proveerlos el lenguaje para evitar la fragmentación y la proliferación de bibliotecas estándar de facto
Escribir librerías independientes del ejecutor no es especialmente difícil, pero sí exige atención constante, y no es algo que toda la comunidad mantenga siempre
Excelente artículo. Me encantan este tipo de análisis profundos de optimización, y ojalá el proyecto también logre bien sus objetivos
A veces he sentido que el compilador no invierte tanto esfuerzo en optimizar los casos “triviales”
Aun así, el título es demasiado dramático para lo que realmente dice el contenido. Igual habría hecho clic si se llamara “Async Rust Optimizations the Compiler Still Misses”
Ahora ya se puede usar async en traits y closures, pero eso es una actualización del sistema de tipos, no un cambio en la maquinaria de async en sí. Waker también se volvió un poco más fácil de manejar, pero eso está más cerca de una mejora de std/core
Según entiendo, la gente que llevó async Rust a tierra terminó bastante quemada y bajó mucho su actividad, y casi nadie tomó realmente el relevo. Aun así, me alegra bastante ver que gente de Google dejó abierto un PR para optimizar la disposición en memoria de las variables capturadas
Mis colegas y yo usamos mucho async, así que quizá tengamos que hacerlo nosotros mismos, o al menos empezar. Se parece más al tipo de “gratis” de “tener un cachorro es gratis”
Así que sí, el título es un poco bait, pero aun así no pienso retirarlo
El autor parece obsesionado con el overhead de funciones triviales. Le molestan los costos extra de los estados “panic” y “returned”, pero eso no es un gran problema
La mayoría de los bloques async útiles son lo bastante grandes como para que el overhead de los casos de error quede diluido
Sobre la falta de inlining, puede que tenga razón. Pero lo que normalmente limita una gran cantidad de actividades es más bien el espacio de estado que requiere cada una
En general, async me parece una idea menos madura. El código normal ya era asíncrono
Si tienes que esperar una tarea async, el hilo se duerme hasta que esté lista, y el kernel abstrae eso. Pero como a la gente no le gustaba estructurar el código con hilos lógicos, se añadió un sistema de callbacks para eventos, y después nos dimos cuenta de que los callbacks son difíciles de razonar y que el control secuencial es mejor
Así que yo diría que los hilos eran el modelo correcto de programación
Ahora los runtimes de lenguaje prefieren los “green threads” por portabilidad y rendimiento, pero la mayoría de los lenguajes no los ofrecen bien. En lugar de eso aparecen problemas como el color async/non-async, scheduling, prioridades y ausencia de preempción. Es un modelo de scheduling y de procesos peor que el de los años 70
El código async también suele escribirse de maneras que no maximizan toda la concurrencia expresable. Por ejemplo, en vez de “ejecuta N tareas de entrada/salida al mismo tiempo”, se escribe algo como “para cada tarea X, await process(x)”
Pero en el mundo de los hilos este problema de concurrencia es todavía peor. Los hilos son inherentemente demasiado pesados, así que expresar concurrencia de forma eficiente es difícil, y no hay manera de optimizarlos en esa dirección
Esta no es una lección nueva. Desde hace mucho se sabe que un ejecutor con work stealing ofrece latencias mucho menores y un P99 más consistente que los hilos tradicionales. Por eso Apple creó GCD a inicios de los 2000
Los hilos no le dan al scheduler del kernel la información más rica que necesitaría para entender la carga de trabajo, y los hilos del kernel son un mecanismo demasiado pesado para conseguir concurrencia fina. Cuando no es cómputo puro sino entrada/salida o cargas mixtas, es aún peor
No todos los programas necesitan ese nivel de rendimiento, pero con el mismo esfuerzo es mucho más fácil alcanzar una vara de rendimiento más alta, y de hecho puedes obtener latencia y throughput que el enfoque tradicional difícilmente alcanza
Que async va en la dirección correcta también se nota en io_uring. El enfoque de I/O de alto rendimiento del kernel con io_uring es completamente distinto al threading y a las system calls tradicionales, y el manejo de finalización se parece mucho más a la concurrencia async. Eso sí, solo con async/await es más difícil aprovecharlo del todo porque no hay suficientes “colores” para expresar las relaciones entre tareas async
La última vez que trabajé con código de corutinas/scheduling, crear un hilo que terminara de inmediato y hacerle join tomaba unos 200µs, mientras que crear, planificar y esperar un green thread propio tomaba unos 400ns
No hace falta esperar 10 años a que alguien diseñe otro framework async absurdamente complejo. En cualquier lenguaje de sistemas puedes hacer tú mismo green threads/corutinas con stack con unas 20 líneas de ensamblador
Optimizar código orientado a ancho de banda es un problema de diseño del scheduling. En el modelo clásico multihilo, el control del scheduling es limitado, mientras que en el modelo async puedes controlarlo casi por completo
Un schedule async bien optimizado puede ser muchísimo más rápido que una arquitectura multihilo equivalente para la misma carga orientada a ancho de banda; ni siquiera hay comparación
Hoy gran parte del código de alto rendimiento está orientado a ancho de banda, y async existe para facilitar la optimización de ese tipo de cargas
Cuando pruebas procesamiento concurrente y quieres verificar que maneja bien las race conditions, con callbacks es mucho más fácil porque puedes controlar el scheduling. Como cada callback representa una unidad separada, puedes ver qué eventos se pueden reordenar y revisar distintos órdenes más fácilmente
En cambio, con hilos es fácil ignorar el orden y dejar de pensar en cuándo la complejidad que ocurre en otros hilos puede afectar al hilo actual. No es tanto simplicidad como simplificación
Además, es difícil probar cambiando realmente los escenarios concurrentes, a menos que metas barreras artificiales para detener hilos o reemplaces la entrada/salida con stubs y pases mocks con callbacks para controlar el orden
El problema de los callbacks es que el stack trace capturado no es el stack lógico de llamadas. Si no usas alguna librería/runtime que haya trabajado en hacerlo significativo, necesitas una buena definición de errores
Claro, también puedes mezclar ambos paradigmas y terminar con lo peor de los dos
Si el objetivo principal de Rust es la seguridad, no entiendo por qué existe panic. Debería poder demostrarse que no hay ninguna ruta posible hacia un panic en el código
Estuve viendo esto toda la semana, y es muy difícil crear un programa que garantice que jamás hará panic. Según entiendo, el panic handler ocupa unos 300KB, y la única forma de excluirlo es que en tiempo de compilación no exista ninguna ruta capaz de panic en el código. Tener que revisar el binario después de compilar para ver si el panic handler quedó incluido se siente como un hack
Se puede prohibir
unwrapy otras operaciones que hacen panic con lints, pero si existiera un subconjunto no-panic de Rust, muchos de los problemas tratados en este artículo desapareceríanEs frustrante tratar con un lenguaje donde hay demasiadas operaciones que, en teoría, pueden panic, incluso en situaciones que en la práctica no ocurrirían salvo al nivel de un bit flip. Pasa igual cuando intentas demostrar que un arreglo no está vacío o cuando lidias con async
Al final terminas metiendo un montón de manejo de errores para situaciones que jamás van a pasar, o usando estructuras raras como el patrón de lista no vacía con un primer campo y el resto por separado. Y aun esa estructura añade su propia hinchazón
También avanza lentamente el trabajo para ampliar los usos basados en pruebas, incluyendo pruebas de que un arreglo no está vacío y cosas así
Si no existiera panic y hubiera que seguir ejecutando en toda situación, entonces en cualquier lugar donde revisas invariantes para recuperarte de algo como corrupción de memoria que rompe invariantes, tendrías que meter mucho manejo de errores
Eso es exactamente el mismo tipo de problema que te preocupa: enormes cantidades de manejo de errores para situaciones que casi nunca ocurren
Cansa un poco esa actitud de esperar que las herramientas vuelvan imposible todo fallo sin que uno mismo quiera involucrarse en hacer nada. Se quiere una API fácil, y si no es suficientemente fácil, entonces se quiere “programar” contenedores de Kubernetes en YAML, y si eso tampoco es suficientemente fácil, se quiere un servicio de hosting a clics de GCP o Amazon
Al final se parece menos a querer programar y más a querer consumir aplicaciones que no fallen, y ese estilo de vida solo existe sobre una relación de simbiosis con la gente que sí construye cosas
Este tipo de discusión fea pero necesaria también ha estado presente por un tiempo en C++
Desde que async llegó a Rust no me gustó su naturaleza contagiosa
Quiero que a Rust le vaya bien, y si aparece más gente así, el futuro de Rust puede verse más prometedor
Hace poco empecé a trabajar con async en Rust, y el principal problema que estoy sufriendo ahora mismo es la duplicación de código
Cada función que quiero que soporte tanto una API asíncrona como una bloqueante tengo que escribirla dos veces. Me gustaría que existiera
maybe-asyncTraté de esquivarlo mirando crates como maybe-async y bisync, pero todos tenían problemas o restricciones fuertes
asyncoconstHoy, la mejor opción para escribir código que quiera vivir tanto en el mundo síncrono como en el asíncrono es sans-io. Thomas Eizinger, de Fireguard, escribió un buen artículo sobre este patrón[1]
Este patrón no solo resuelve de forma elegante el problema sync/async, también facilita las pruebas y abre la puerta a técnicas como DST[2]
Yo también escribí un artículo sobre esto[3], donde remarco que el problema va más allá de async vs sync e incluye una cuestión más amplia entre distintos ejecutores
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
asyncya esmaybe-asyncLa diferencia entre
fn -> voidyfn -> Futurees que la primera termina de ejecutarse inmediatamente hasta el final, mientras que la segunda puede terminar más tardeSi quieres ejecutar una función async de forma bloqueante, basta con usar un ejecutor bloqueante
Lo que me gusta de este artículo es que también me permitió ver los objetivos de Rust para 2026
En el equipo usamos Rust, pero no hemos necesitado meternos tan a fondo para hacer lo que necesitamos. Aun así, es entretenido ver cómo un lenguaje con tanto feedback de la comunidad va desarrollándose desde la base
No sentí algo así con C++, y en otras áreas tampoco sé bien cómo funciona
Lo que sí me deja una sensación rara es que cada objetivo parece necesitar financiamiento específico, así que se siente un poco como Kickstarter. Me pregunto si de verdad este es el mejor modelo que se ha encontrado hasta ahora
Un objetivo de proyecto es un sistema donde una persona o un grupo pequeño expresa que quiere trabajar en algo, y pide a voluntarios del proyecto Rust tiempo de apoyo continuo, como revisión de código o respuesta a preguntas
Eso no significa que el proyecto Rust como tal haya fijado ese objetivo ni que necesariamente lo respalde
Así que no sería correcto verlo como la hoja de ruta oficial de Rust; es más preciso verlo como “hay contribuyentes interesados en trabajar en esta área”
Cuando una tecnología se establece comercialmente, por desgracia parece que las cosas tienden a ir por ahí. Tampoco es fácil culpar a grandes patrocinadores por financiar solo las partes que les interesan
Tengo entendido que una parte importante del financiamiento de TweedeGolf viene del gobierno neerlandés
Las nuevas funcionalidades se pueden “vender”. Cuestan dinero construirlas, pero resuelven problemas reales, y si el costo del problema es mayor que el costo de desarrollar la funcionalidad, las empresas por lo general están dispuestas a pagar
El mantenimiento es más difícil, pero ahora también existen fondos para maintainers. Un ejemplo es el fondo de RustNL: https://rustnl.org/maintainers/
Estos fondos apuntan a trabajo más amplio y sostenido, y se sostienen con pequeños aportes de varias organizaciones
No sé si sea el mejor modelo, pero al menos parece funcionar hasta cierto punto
Si lees la documentación de Rust Async y Tokio, explican bien por qué no debes meter partes intensivas en CPU dentro del stack async, cómo usar eficientemente herramientas básicas como
std::sync::Mutexdentro de bloques async, y cómo unir código síncrono con código asyncMucho código no sigue estas guías porque no le importa la eficiencia o no la necesita. Pero hay muchos proyectos que sí valoran rendimiento y eficiencia, y una vez que el código corre en producción te das cuenta de las trampas. ScyllaDB es un ejemplo
Los LLM tampoco ayudan. Generan todo como async hasta
main, usan mal las herramientas básicas y no diseñan correctamente el sistemaEl plegado de estado duplicado, es decir, el patrón de sacar el
matchfuera de la rama con await como en el ejemplo deprocess_command, es probablemente la forma más fácil que cualquiera puede aplicar hoy al código async existenteNo requiere trabajo del compilador, solo refactorización
Sobre la parte de que “los Future no se inlinean fácilmente”, en un lenguaje de programación que hice escribí un pase personalizado que inlinea llamadas a funciones async dentro de funciones async
En general funciona bien y elimina parte del boilerplate, pero el tamaño del binario resultante crece bastante
Técnicamente, Rust también podría hacer lo mismo
Comentarios de Lobste.rs
Fue un texto mucho más constructivo de lo que esperaba viendo solo el título
Ojalá quien quiera trabajar en esto reciba el apoyo que necesita
Me alegra ver que se esté abordando este problema. Ya había leído varias veces que ahora mismo rustc le está pasando demasiado código a LLVM y espera que el optimizador resuelva todo, y este texto en particular además está pidiendo financiamiento para ese trabajo
Dios mío, yo era un idiota
Siempre pensé que async era intrínsecamente “inflado”, porque de una forma u otra necesita un runtime, seguimiento de tareas y polling para verificar la finalización. Ese overhead no es cero, al fin y al cabo
Lo que se quería decir aquí con “abstracción de costo cero” era sobre la funcionalidad del lenguaje, y yo lo veía como algo aparte del runtime agregado
Ni siquiera se me había ocurrido mirar qué está emitiendo rustc antes de pasárselo a LLVM
Para quienes no están familiarizados con async Rust:
Esto es totalmente cierto. Incluso un árbol anidado de llamadas async, después de una optimización máxima, se solidifica internamente en una sola estructura con una máquina de estados dentro. Es una forma realmente ingeniosa de hacerlo
¿En un build de release, llegar a este caso genera una especie de deadlock? ¿O también podría producir fugas porque hay tareas esperando trabajo que siempre queda en
Pending?No puedes hacer polling incorrecto con
.awaitSe me ocurren algunas cosas:
panic=unwind. Fuera de algunos test harnesses, casi nunca he visto ventajas frente apanic=abortque compensen su costo. Incluso en un test harness, en Linux parece que podría aplicarse una elección parecida usandoclonede una forma rebuscada para hacerwaital hilo de ejecución en vez depthread_join. Puede que yo esté equivocado en esto¿El enlace también se le cayó a alguien más hace un momento?
Edit: la entrada del blog se ve como medio segundo y luego te manda a una página 404
Edit 2: entré a la lista de posts del blog y estuve picándole a varias cosas, y hasta abriendo ese post desde la lista igual me manda a una página 404. ¿Cómo se puede arruinar así un blog que es una página estática, o al menos debería serlo?
Como referencia, creo que seguí los mismos pasos de reproducción y a mí no me salió ningún 404. Lo probé en celular y escritorio, con JavaScript activado y desactivado. Así que da la impresión de que lo que te pasó pudo haber sido más complejo de lo que parecía