1 puntos por GN⁺ 2025-05-23 | 1 comentarios | Compartir por WhatsApp
  • Se detectó que el decodificador AV1 rav1d, escrito en Rust, es aproximadamente un 9% más lento que dav1d, basado en C
  • Se comprobó que la optimización de la inicialización de buffers y la mejora de la lógica de comparación de estructuras aportan por separado mejoras de velocidad de 1.5% y 0.7%, respectivamente
  • Se utilizó la herramienta de perfilado samply para identificar con precisión las causas de la diferencia de rendimiento entre ambas versiones
  • En Rust, se aumentó la eficiencia reemplazando la implementación predeterminada de PartialEq por una comparación a nivel de bytes
  • Con esta optimización se mejoró alrededor del 30% de la diferencia total de rendimiento, aunque todavía queda margen para seguir optimizando

Antecedentes y enfoque

  • rav1d es un proyecto que porta el decodificador AV1 dav1d a Rust con c2rust, incorporando además funciones optimizadas en asm y mejoras de seguridad propias del lenguaje Rust
  • Ya existe una referencia pública de rendimiento base, y el rav1d basado en Rust sigue siendo alrededor de un 5% más lento que el dav1d basado en C
  • En lugar de analizar la estructura general de un decodificador de video complejo, el análisis se centró en las diferencias de tiempo de ejecución binario con la misma entrada
  • La comparación se realizó de forma sistemática con la herramienta de medición de rendimiento (hyperfine) y el profiler (samply)
  • El entorno objetivo fue un chip macOS M3, simplificado con ejecución en un solo hilo

Medición de rendimiento: comparación base

  • Se compiló y evaluó cada versión con el mismo archivo de prueba (Chimera-AV1-8bit-1920x1080-6736kbps.ivf)
  • rav1d: aproximadamente 73.9 segundos, dav1d: aproximadamente 67.9 segundos, lo que mostró una diferencia de cerca de 6 segundos (9%) en tiempo de ejecución
  • Cada compilador (Clang, Rustc) usa prácticamente la misma versión de LLVM

Análisis de perfilado

  • Con el profiler samply se comparó la cantidad de muestras por función en cada ejecutable
  • Se revisaron especialmente las rutas de llamada y la distribución de muestras de funciones en ensamblador basadas en NEON (ARM SIMD)
  • dav1d separa las funciones de filtro y llama condicionalmente a funciones asm, mientras que rav1d administra todo desde una sola función de dispatch
  • La cantidad de muestras Self de la función cdef_filter_neon_erased mostró unas 270 muestras más que la suma de las dos funciones equivalentes en dav1d (alrededor del 1% del total)
  • El análisis permitió detectar una sección donde se inicializaba de forma innecesariamente grande un buffer temporal (zero-initialized buffer)

Optimización eliminando la inicialización del buffer

  • Por seguridad, Rust realiza zeroing automático con formas como [0u16; LEN]
  • Sin embargo, en C (dav1d) el buffer no se rellena explícitamente con ceros, sino que solo se escriben valores en la parte realmente utilizada
  • En Rust se eliminó el costo innecesario de inicialización usando std::mem::MaybeUninit
  • Las muestras Self de la función cdef_filter_neon_erased bajaron significativamente de 670 a 274
  • Otro buffer grande de Align16 también movió su inicialización fuera del bucle para reducir ese costo a una sola vez
  • Después de la optimización, el benchmark bajó a aproximadamente 72.6 segundos, una mejora de 1.2 segundos (1.5%)

Optimización de comparación de estructuras

  • El análisis de inverted stack en el perfilado reveló que la función add_temporal_candidate estaba funcionando de forma más ineficiente de lo esperado
  • La comparación de campos de la estructura Mv dentro de esa función (implementación automática de PartialEq) generaba código innecesariamente lento
  • En C se usa un union para realizar una comparación eficiente en unidades de uint32_t
  • En Rust, evitando unsafe, se implementó una comparación por slices de bytes mediante el trait zerocopy::AsBytes
  • Esta optimización aportó otra mejora de 0.5 segundos (aproximadamente 0.7%)

Resultados y cierre

  • Con dos optimizaciones simples (eliminación de la inicialización del buffer y comparación de estructuras por bytes) se logró reducir el tiempo de ejecución en más de un 2%
  • Aun así, sigue quedando una diferencia de rendimiento de alrededor del 6%, por lo que hay bastante margen para optimizaciones adicionales
  • Se confirmó la efectividad del método de comparación entre snapshots del profiler
  • Hay alta probabilidad de seguir optimizando rav1d y dav1d a partir del análisis de snapshots
  • Gracias a la retroalimentación activa y la colaboración de los mantenedores, se lograron mejoras sin comprometer la seguridad

Resumen

  • Se analizó con precisión la diferencia de 6 segundos (9%) en tiempo de ejecución entre rav1d y dav1d usando herramientas de profiler (samply) y benchmark (hyperfine)
  • Dos optimizaciones principales:
    • Eliminación del zeroing innecesario de buffers en código especializado para ARM (1.2 segundos, -1.6%)
    • Cambio de la implementación de PartialEq en estructuras numéricas pequeñas por una comparación rápida por bytes (0.5 segundos, -0.7%)
  • Cada optimización fue concisa, de apenas unas decenas de líneas, sin agregar nuevo código unsafe
  • Con colaboración de los mantenedores y revisión de PR, se mejoraron al mismo tiempo la confiabilidad y la calidad
  • Aún queda una brecha de rendimiento de aproximadamente 6%, así que hay bastante espacio para seguir investigando optimizaciones comparativas basadas en profiler

Go ahead and give this a try! Maybe rav1d can eventually become faster than dav1d 👀🦀.

1 comentarios

 
GN⁺ 2025-05-23
Comentarios de Hacker News
  • Se compartió la opinión de que el problema de comparar dos u16 es un tema interesante, junto con el enlace al issue relacionado https://github.com/rust-lang/rust/issues/140167
    • Expresó sorpresa de que no se mencionara el store forwarding en la discusión; el resultado de generación de código con -O3 es excesivo, pero con -O2 parece una decisión razonable. Explicó en concreto que, si una de las estructuras acaba de ser operada, intentar una carga de 32 bits puede hacer fallar el store forwarding, con lo que la mejora de rendimiento perdería sentido. También señaló que, en escenarios sin inlining y sin PGO, el compilador carece de la información necesaria para juzgar si la optimización conviene
    • Comentó que le gustó que la discusión del issue no estuviera llena de comentarios del tipo “a mí también me pasó” o “¿cuándo lo arreglan?”, y compartió con franqueza que, como desarrollador web, los issues de GitHub le resultan insatisfactorios
    • Opinó que este caso muestra lo complejo que es el desarrollo de compiladores, y expresó su convicción de que los compiladores de la familia C tampoco manejarían mejor este tipo de problemas
  • Expresó curiosidad por cómo insertaron los resultados del perfilador en el post del blog, y preguntó si simplemente copiaron los nodos HTML tal cual
  • Comentó que le pareció interesante que este artículo sobre las ventajas de rendimiento de omitir la inicialización del búfer a cero apareciera pocos días después de otra publicación relacionada, y compartió el enlace al post anterior https://news.ycombinator.com/item?id=44032680
  • Señaló que el título del artículo es demasiado tímido en comparación con los resultados reales, y subrayó que en realidad hubo una mejora de velocidad de 2.3% gracias a dos buenas optimizaciones
    • Opinó que la mejora de 1.5% solo aplica a aarch64, así que mencionarla como cifra global no es del todo justo; sostuvo que, considerando la proporción entre ARM y x86, lo correcto sería verla como aproximadamente la mitad
  • Evaluó que la publicación fue útil y que le impresionó el hallazgo de código ineficiente en la comparación de pares de enteros de 16 bits
    • Se preguntó si los desarrolladores de Rust/LLVM podrían aplicar esta optimización automáticamente cuando sea posible, y mencionó que en Rust la información relacionada con la inicialización de memoria es mucho más precisa
  • Consideró que, en igualdad de condiciones, este tipo de códecs debería tratarse en un lenguaje como WUFFS o en un lenguaje especializado equivalente, más que en Rust. Compartió la impresión de que convertir código tan complejo como dav1d a WUFFS sería muchísimo más difícil que simplemente traducir y limpiar el código C existente. Aun así, sostuvo que vale la pena intentarlo y que es una inversión digna a nivel civilizatorio
    • Explicó que WUFFS es adecuado para parsear contenedores como Matroska, webm y mp4, pero no sirve en absoluto para decodificadores de video. Señaló que, al no haber asignación dinámica de memoria, manejar datos dinámicos se vuelve complicado, y enfatizó que los códecs de video no solo parsean archivos, sino que requieren administrar una gran variedad de estados dinámicos
  • Comentó, casi como hablando solo, que tenía curiosidad por el progreso de la recompensa de rav1d, y expresó empatía al ver que había otra persona con la misma inquietud
  • Dijo que un artículo que empieza con un meme divertido suele ser una buena publicación, y mencionó su relación con la discusión reciente sobre la “recompensa de $20 mil por optimización en Rust del decodificador AV1 Rav1d”, añadiendo el enlace relacionado https://news.ycombinator.com/item?id=43982238
    • Bromeó diciendo que este caso es un ejemplo claro de Nominative determinism (efecto del nombre)
  • Comentó que, sinceramente, la primera optimización le pareció algo sorprendente porque es un tipo de hallazgo común que suele encontrarse fácilmente con perf; pensaba que el tema del zeroing ya se había discutido en el primer post. Dijo que la segunda optimización era más compleja e interesante, pero aun así también fue guiada por perf, y aconsejó no subestimar la utilidad de la herramienta
    • Aclaró que no se usó solo perf, sino que también se recurrió a perfilado diferencial entre la versión en C y la versión en Rust, además de un proceso manual de emparejamiento. Señaló que perf diff existe, pero tiene la limitación de que, al diferir los nombres de los símbolos, el emparejamiento automático resulta difícil
    • Mencionó que abordó el tema desde la perspectiva de dispositivos Apple basados en aarch64, y subrayó desde su experiencia que personas con contextos distintos pueden detectar rápidamente cosas que, vistas después, parecen “obvias”
  • Especuló que este tema podría ser la razón por la cual la cuenta de Twitter de ffmpeg terminó pronunciándose sobre asuntos relacionados con Rust, y compartió el enlace al tuit https://x.com/ffmpeg/status/1924137645988356437?s=46
    • Compartió con franqueza que, al leer la cuenta de Twitter de ffmpeg, empezó a sentirse escéptico sobre usar ffmpeg. Lamentó que no haya alternativas, criticó que la comunidad de desarrolladores sea muy tóxica, y señaló que, aunque el rendimiento máximo puede ser importante, en entornos donde se intercambian datos con el exterior es posible que ffmpeg tenga varias vulnerabilidades remotas (CVE) cada año. Enfatizó la necesidad de un sandbox estricto desde la perspectiva de seguridad, y opinó que hace falta un punto medio donde se construyan soluciones que sean rápidas y seguras a la vez, compartiendo el enlace relacionado https://ffmpeg.org/security.html
    • Propuso que una mejor respuesta sería contestar con mejoras de rendimiento en dav1d; usó la analogía de los récords deportivos para decir que mejorar números de forma aislada tiene menos impacto que lograr un verdadero nuevo récord. Explicó de forma amena que la solución real sería obtener resultados sustancialmente más rápidos e innovadores