Cómo resolver con arenas de Rust un problema de rendimiento de 3 años en Gleam
(giacomocavalieri.me)- Al cambiar las asignaciones individuales en el heap que se repetían en la estructura recursiva
Documentdel formateador de Gleam por referencias basadas en arena, se resolvió un problema pendiente de mejora de rendimiento que llevaba 3 años abierto - Se reemplazó
Box<Self>por&Selfalmacenado en una arena, y con la verificación de lifetimes de Rust se garantiza que los datos internos no puedan referenciarse después de que la arena haya sido eliminada - Al asignar una sola vez y reutilizar cientos de
Documentrepetidos, como palabras clave del lenguaje y comas, el tiempo de ejecución del pretty printer bajó de 13ms a 9.8ms, una mejora del 24% - El tiempo total de ejecución de
gleam format, incluyendo lectura del código fuente y parsing, también se redujo en 13%, y el uso máximo de memoria bajó de 8.4MB a 7.6MB, cerca de un 10% - Aunque fue necesario modificar ampliamente funciones y sitios de llamada para pasar la arena, con un trabajo manual de
+2963/-1032, fue posible mejorar tanto la velocidad como el uso de memoria reduciendo las asignaciones individuales repetitivas
El costo de asignación en la estructura recursiva Document
- El pretty printer de Gleam usa una estructura de datos recursiva llamada
Documentpara representar cómo debe imprimirse el código y dónde dividirlo cuando se supera el límite de línea - Sus variantes principales incluyen
String, que contiene el texto a imprimir;Break, que define las cadenas antes y después del salto de línea;Group, que agrupa varios documentos; yNest, que aumenta la indentación al hacer un salto de línea - Incluso una lista como
[1, 2]se representa combinandoString,Break,NestyGroup- Si cabe en la línea actual, se imprime en una sola línea como
[1, 2] - Si supera el límite de línea, se divide en varias líneas según el
Breakespecificado, con indentación y mostrando también la coma final
- Si cabe en la línea actual, se imprime en una sola línea como
- Las variantes que contienen otro
Documentinternamente, comoNest, usaban antesBox<Self>, por lo que cada documento anidado requería una asignación individual en el heap, y ese trabajo podía ocupar una parte importante del tiempo de ejecución - El algoritmo base del pretty printer de Gleam puede consultarse en el paper Strictly Pretty
Reemplazar Box por referencias de arena
- Se cambió la estructura de
Nest(Box<Self>)aNest(&'doc Self)para que referencie otroDocumentalmacenado en la arena - Para poder guardar referencias, se agregó a
Documentel lifetime'docde los datos del documento, separado del lifetime'stringde las cadenas - Para la implementación se utilizó el crate
typed_arena- Al guardar un valor en la arena con
alloc, se devuelve una referencia a ese valor - Los datos asignados pueden usarse mientras la arena siga viva
- Cuando la arena sale de alcance, los datos internos también se eliminan junto con ella
- Al guardar un valor en la arena con
- La referencia devuelta por
alloc(&self, value: T) -> &mut Tno puede vivir más que la arena, y el borrow checker de Rust bloquea referencias inválidas después de que esta se destruye
Asignar una vez y reutilizar documentos repetidos
- La arena no solo se usó para referenciar otros documentos, sino también para cachear
Documentrepetidos en todo el código - Ya no hace falta crear una y otra vez documentos como
String("fn"),String("pub")oString("type")para representar palabras clave del lenguaje - Documentos como
Break { unbroken: ", ", broken: "," }, usados entre elementos de una lista, también se asignan una sola vez y luego se reutilizan - Así, cientos de documentos pequeños que antes había que meter continuamente en
Boxahora pueden asignarse una sola vez cada uno
Una API simple, pero cambios extensos en los sitios de llamada
- Aunque la API de la arena en sí es simple, el proceso de migrar una gran parte del código existente requirió cambios repetitivos
- Las funciones que antes solo llamaban a
Box::newahora también necesitan recibir un argumento de arena donde asignar los datos - Funciones como
format_listahora pasan la misma arena a las funciones internas que formatean los elementos de la lista, y crean documentos anidados conarena.allocen lugar deBox::new - Tras varios días de buscar y reemplazar con cuidado, el trabajo se completó en un pull request de
+2963/-1032 - Fue un trabajo repetitivo y manual, y en este proceso no se utilizó ningún LLM
Mejora en velocidad y uso máximo de memoria
- Al formatear
squirrel, un proyecto real de Gleam, el tiempo de ejecución del pretty printer bajó de 13ms a 9.8ms- Comparando solo el pretty printer, eso representa una mejora de rendimiento del 24%
- Como
gleam formatno solo formatea, sino que también lee y parsea el código fuente del proyecto, el pretty printer ocupa solo una parte del trabajo total, pero aun así el tiempo total de ejecución también se redujo en 13% - El uso máximo de memoria bajó de 8.4MB a 7.6MB, aproximadamente un 10%
- Al reducir el número de asignaciones individuales en el heap mediante arenas, se mejoraron tanto la velocidad de ejecución del formateador como el uso de memoria
1 comentarios
Opiniones en Lobste.rs
Si se quiere reducir otro 10% el uso máximo de memoria, valdría la pena considerar otra arena que devuelva handles opacos en lugar de referencias. Los handles pueden ser de 16 o 32 bits en vez de 64 bits, lo que tiene un gran efecto cuando hay muchas asignaciones pequeñas.
Según https://donsz.nl/blog/arenas/, el único crate que soporta esto es
compact_arena, pero si no se ajusta a las necesidades, tampoco es difícil implementarlo directamente.Option<T>separándolas en poco más de 1 bit por elemento para indicar presencia y losTde los elementos que realmente existen.A cambio, devuelve
Option<&T>como tipo de referencia, no&Option<T>; de forma más general, es un enfoque de recursión estructural siguiendo referencias, en lugar de poner referencias dentro de la recursión estructural.Las arenas merecen usarse más ampliamente, y devolver referencias en lugar de handles o smart pointers hace que el pattern matching de enums recursivos en Rust sea muy cómodo. Normalmente, aunque uno quiera escribir algo como
if let MyAst::Add(MyAst::Multiply(a, b), c) = expr {}, no es posible por las cajas o punteros intermedios, pero las referencias se pueden usar de forma transparente en pattern matching sin funciones de nightly. Si todas las referencias apuntan a una misma arena, la vida útil que conecta todo el árbol también se mantiene de forma consistente.Este método se parece bastante al enfoque que soporta
pretty, otra biblioteca de pretty printing al estilo Wadler. Sin embargo, como soporta varios métodos de asignación de memoria, la API puede sentirse algo pesada.Sorprende que el autor del paper citado en el artículo trabaje en una empresa de TI común y corriente en Brunswick.