1 points par GN⁺ 2025-05-23 | 1 commentaires | Partager sur WhatsApp
  • Il a été constaté que le décodeur AV1 rav1d, écrit en Rust, était environ 9 % plus lent que dav1d, basé sur C
  • Des optimisations de l’initialisation des buffers et de la logique de comparaison des structures ont permis des gains de vitesse individuels de 1,5 % et 0,7 % respectivement
  • L’outil de profilage samply a été utilisé pour identifier précisément les causes de l’écart de performances entre les deux versions
  • À la place de l’implémentation par défaut de PartialEq en Rust, une comparaison au niveau des octets a été utilisée pour améliorer l’efficacité
  • Cette optimisation a permis de réduire d’environ 30 % l’écart global de performances, même s’il reste encore de la marge pour d’autres optimisations

Contexte et approche

  • rav1d est un projet qui porte le décodeur AV1 dav1d en Rust via c2rust, tout en intégrant des fonctions optimisées en asm et des améliorations de sûreté propres au langage Rust
  • Une référence de performance publique a été définie, et rav1d en Rust reste environ 5 % plus lent que dav1d en C
  • L’analyse s’est concentrée non pas sur la structure globale d’un décodeur vidéo complexe, mais sur les différences de temps d’exécution binaire avec une entrée identique
  • Une comparaison systématique a été menée à l’aide d’outils de mesure de performances (hyperfine) et d’un profiler (samply)
  • L’environnement ciblé était une puce macOS M3, avec une exécution en mono-thread pour simplifier l’analyse

Mesure des performances : comparaison de base

  • Les builds et benchmarks ont été réalisés avec le même fichier de test (Chimera-AV1-8bit-1920x1080-6736kbps.ivf)
  • rav1d : environ 73,9 secondes, dav1d : environ 67,9 secondes, soit un écart d’environ 6 secondes (9 %) sur le temps d’exécution
  • Chaque compilateur (Clang, Rustc) utilisait pratiquement la même version de LLVM

Analyse de profilage

  • Le profiler samply a servi à comparer, pour chaque exécutable, le nombre d’échantillons par fonction
  • Les chemins d’appel et la distribution des échantillons dans les fonctions assembleur basées sur NEON (ARM SIMD) ont été examinés en priorité
  • dav1d sépare les appels asm via des fonctions de filtre distinctes, tandis que rav1d gère l’ensemble via une seule fonction de dispatch
  • La fonction cdef_filter_neon_erased présentait environ 270 échantillons Self de plus que la somme des deux fonctions équivalentes dans dav1d (soit environ 1 % du total)
  • L’analyse a montré qu’une zone d’initialisation d’un buffer temporaire (zero-initialized buffer) était inutilement trop grande

Optimisation par suppression de l’initialisation des buffers

  • Pour des raisons de sûreté, Rust effectue automatiquement un zeroing avec des écritures comme [0u16; LEN]
  • En revanche, C (dav1d) ne zero-initialise pas explicitement le buffer et n’écrit que dans la zone réellement utilisée
  • En Rust, std::mem::MaybeUninit a permis d’éliminer le coût de cette initialisation inutile
  • Les échantillons Self de cdef_filter_neon_erased sont tombés de 670 à 274
  • Un autre gros buffer Align16 a lui aussi vu son initialisation déplacée hors de la boucle, réduisant ce coût à une seule fois
  • Après optimisation, le benchmark est passé à environ 72,6 secondes, soit un gain de 1,2 seconde (1,5 %)

Optimisation de la comparaison de structures

  • L’analyse des piles inversées du profiler a révélé que la fonction add_temporal_candidate fonctionnait de manière plus inefficace que prévu
  • La comparaison des champs de la structure Mv dans cette fonction (implémentation automatique de PartialEq) générait du code inutilement lent
  • En C, une union est utilisée pour effectuer une comparaison efficace sur des unités de type uint32_t
  • En Rust, sans recourir à unsafe, une comparaison par tranches d’octets a été implémentée avec le trait zerocopy::AsBytes
  • Cette optimisation a apporté un nouveau gain de 0,5 seconde (environ 0,7 %)

Résultats et synthèse

  • Deux optimisations simples (suppression de l’initialisation des buffers, comparaison de structures par octets) ont permis de réduire le temps d’exécution de plus de 2 %
  • Il reste encore environ 6 % d’écart de performances, ce qui laisse une marge importante pour d’autres optimisations
  • L’approche par comparaison entre instantanés du profiler s’est révélée efficace
  • Le potentiel d’optimisations supplémentaires fondées sur l’analyse des instantanés de rav1d et dav1d est élevé
  • Grâce aux retours actifs et à la collaboration des mainteneurs du projet, des améliorations ont pu être mises en œuvre sans compromettre la sûreté

Résumé

  • Les outils de profilage (samply) et de benchmark (hyperfine) ont permis d’analyser finement l’écart de 6 secondes (9 %) entre rav1d et dav1d
  • Deux optimisations principales :
    • suppression d’un zeroing inutile de buffer dans du code spécifique ARM (1,2 seconde, -1,6 %)
    • remplacement de l’implémentation PartialEq d’une petite structure numérique par une comparaison rapide d’octets (0,5 seconde, -0,7 %)
  • Chaque optimisation reste concise, en quelques dizaines de lignes, sans nouveau code unsafe
  • La collaboration avec les mainteneurs et la revue de PR ont permis d’améliorer à la fois la fiabilité et la qualité
  • Il reste encore environ 6 % d’écart de performances, ce qui laisse largement de quoi poursuivre les optimisations guidées par le profiler

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

1 commentaires

 
GN⁺ 2025-05-23
Réactions sur Hacker News
  • Partage de l’avis selon lequel le problème de comparaison de deux u16 est un sujet intéressant, avec un lien vers l’issue correspondante https://github.com/rust-lang/rust/issues/140167
    • Étonnement que le store forwarding n’ait pas été mentionné dans la discussion ; le résultat de génération de code en -O3 semble excessif, mais en -O2 cela paraît raisonnable ; explication concrète que si l’une des structures vient juste d’être manipulée, tenter un chargement 32 bits peut faire échouer le store forwarding et rendre le gain de performance nul ; remarque qu’en situation non inline / sans PGO, le compilateur manque d’informations pour juger si l’optimisation est appropriée
    • Impression positive que la discussion de l’issue ne soit pas remplie de commentaires du type « moi aussi je l’ai eu » ou « quand est-ce que ce sera corrigé ? », et partage d’un avis franc selon lequel, en tant que développeur web, les issues GitHub sont peu satisfaisantes
    • Avis selon lequel ce cas montre à quel point le développement de compilateurs est complexe, avec la conviction que les compilateurs de la famille C ne géreraient pas mieux ce genre de problème
  • Curiosité sur la manière dont les résultats du profiler ont été intégrés dans le billet de blog, avec la question de savoir si les nœuds HTML ont été copiés tels quels
  • Remarque qu’il est intéressant de voir un article sur les gains de performance liés à la suppression de l’initialisation du tampon (zeroing) publié quelques jours après un billet connexe, avec partage du lien de l’article précédent https://news.ycombinator.com/item?id=44032680
  • Observation selon laquelle le titre de l’article est trop modeste par rapport au résultat réel, en soulignant qu’il y a en fait un gain de vitesse de 2,3 % grâce à deux bonnes optimisations
    • Avis que l’amélioration de 1,5 % ne concerne que aarch64, donc qu’il n’est pas tout à fait équitable de la présenter comme un chiffre global ; affirmation qu’en tenant compte de la part d’arm/x86, il vaudrait mieux la considérer comme environ la moitié
  • Évaluation selon laquelle le billet était instructif et que la découverte d’un code inefficace dans la comparaison de paires d’entiers 16 bits était particulièrement marquante
    • Interrogation sur la possibilité pour les développeurs Rust/LLVM d’appliquer automatiquement cette optimisation quand c’est possible, avec la remarque que Rust dispose d’informations bien plus précises sur l’initialisation mémoire
  • Opinion que, toutes choses égales par ailleurs, ce genre de codec devrait être traité non pas en Rust mais dans un langage comme WUFFS ou un langage spécialisé équivalent ; partage du ressenti que convertir un code aussi complexe que dav1d en WUFFS serait bien plus difficile qu’un simple nettoyage/traduction de code C existant ; malgré cela, l’idée est jugée digne d’intérêt et d’un investissement à l’échelle de la civilisation
    • Explication que WUFFS convient à l’analyse de conteneurs comme Matroska, webm ou mp4, mais pas du tout à un décodeur vidéo ; absence d’allocation mémoire dynamique, ce qui rend le traitement de données dynamiques difficile ; insistance sur le fait qu’un codec vidéo ne se contente pas de parser un fichier mais doit gérer une très grande variété d’états dynamiques
  • Question lancée à voix haute sur l’avancement de la prime rav1d, avec une expression d’empathie envers le fait que d’autres se posent la même question
  • Impression qu’un article qui commence par un mème amusant est généralement un bon billet, avec mention du lien avec la récente discussion « prime de 20 000 $ pour l’optimisation Rust du décodeur AV1 Rav1d » et ajout du lien correspondant https://news.ycombinator.com/item?id=43982238
    • Avis teinté d’humour selon lequel il s’agit d’un cas clair de « déterminisme nominatif »
  • Honnêtement, surprise que la première optimisation ait été présentée ainsi, car c’est un type de cas assez courant qu’on peut repérer facilement en utilisant bien perf ; l’auteur pensait que le problème de zeroing avait déjà été abordé dans le premier billet ; la deuxième optimisation était plus complexe et plus intéressante, mais elle aussi a été guidée par perf, d’où le conseil de ne pas sous-estimer l’utilité de l’outil perf
    • Précision qu’il ne s’agissait pas seulement d’utiliser perf, mais aussi d’un processus de profilage différentiel entre la version C et la version Rust, puis d’un appariement manuel pour identifier le problème ; mention de la fonction perf diff, mais aussi de sa limite puisque les noms de symboles diffèrent, ce qui rend l’appariement automatique difficile
    • Mention du fait que l’approche venait d’une perspective centrée sur les appareils Apple en aarch64 ; insistance, au vu de l’expérience, sur le fait que des personnes ayant des parcours différents peuvent repérer rapidement des éléments qui paraîtront « évidents » avec le recul
  • Supposition que cette affaire soit à l’origine de la prise de position récente du compte Twitter de ffmpeg sur les questions liées à Rust, avec partage du lien vers le tweet https://x.com/ffmpeg/status/1924137645988356437?s=46
    • Sentiment sincère qu’après avoir lu le compte Twitter de ffmpeg, l’utilisation de ffmpeg inspire davantage de réserves ; regret qu’il n’y ait pas vraiment d’alternative ; critique d’une communauté de développeurs jugée très toxique ; remarque que la performance maximale peut être importante, mais que dans des environnements qui échangent des données avec l’extérieur, ffmpeg peut accumuler plusieurs vulnérabilités distantes (CVE) par an ; insistance sur la nécessité d’un sandboxing strict du point de vue sécurité, et avis qu’il faut un juste milieu permettant de construire ensemble des solutions à la fois rapides et sûres, avec partage du lien correspondant https://ffmpeg.org/security.html
    • Suggestion qu’une meilleure réponse serait d’améliorer les performances de dav1d ; comparaison avec les records sportifs, en expliquant que battre encore un peu le chrono impressionne moins qu’un véritable nouveau record ; explication enjouée que la vraie solution serait d’obtenir des résultats réellement plus rapides et innovants