Leistungsverbesserungen beim rav1d-Video-Decoder
(ohadravid.github.io)- Es wurde festgestellt, dass der in Rust geschriebene rav1d-AV1-Decoder gegenüber dem C-basierten dav1d rund 9 % langsamer ist
- Durch die Optimierung der Pufferinitialisierung und die Verbesserung der Struct-Vergleichslogik wurden jeweils einzeln Geschwindigkeitsgewinne von 1,5 % bzw. 0,7 % bestätigt
- Mithilfe des Profiling-Tools samply wurden die Ursachen der Leistungsunterschiede zwischen den beiden Versionen konkret ermittelt
- Statt der Standardimplementierung von PartialEq in Rust wurde die Effizienz durch einen Byte-für-Byte-Vergleich erhöht
- Mit dieser Optimierung wurden rund 30 % des gesamten Leistungsunterschieds verbessert, es bleibt jedoch weiteres Optimierungspotenzial
Hintergrund und Vorgehensweise
- rav1d ist ein Projekt, das den dav1d-AV1-Decoder mit c2rust nach Rust portiert und dabei asm-optimierte Funktionen sowie für Rust typische Sicherheitsverbesserungen einbezieht
- Öffentlich ist ein grundlegender Performance-Maßstab definiert, wobei das Rust-basierte rav1d derzeit etwa 5 % langsamer als das C-basierte dav1d ist
- Statt die Gesamtstruktur eines komplexen Video-Decoders zu untersuchen, konzentrierte sich die Analyse auf Unterschiede in der Laufzeit der Binärdateien bei identischer Eingabe
- Mit Performance-Messtool (hyperfine) und Profiler (samply) wurde ein systematischer Vergleich durchgeführt
- Die Zielumgebung war ein macOS-System mit M3-Chip, vereinfacht durch Ausführung in einem einzelnen Thread
Leistungsmessung: Vergleich der Standardwerte
- Mit derselben Testdatei (Chimera-AV1-8bit-1920x1080-6736kbps.ivf) wurden jeweils Build und Benchmark durchgeführt
- rav1d: etwa 73,9 Sekunden, dav1d: etwa 67,9 Sekunden, also ein Laufzeitunterschied von rund 6 Sekunden (9 %)
- Beide Compiler (Clang, Rustc) verwendeten nahezu dieselbe LLVM-Version
Profiling-Analyse
- Mit dem samply-Profiler wurde die Anzahl der Samples auf Funktionsebene zwischen den beiden Executables verglichen
- Besonderes Augenmerk galt den Aufrufpfaden und der Sample-Verteilung der auf NEON (ARM SIMD) basierenden Assemblerfunktionen
- dav1d verzweigt zu asm-Funktionen über separate Filterfunktionen, rav1d verwaltet alles über eine einzige Dispatch-Funktion
- Bei der Funktion cdef_filter_neon_erased zeigte sich, dass die Zahl der Self-Samples um etwa 270 höher lag als die Summe der zwei Funktionen in dav1d (entspricht insgesamt 1 %)
- Die Analyse zeigte einen Abschnitt, in dem ein temporärer Puffer (zero-initialized buffer) unnötig groß initialisiert wurde
Optimierung durch Entfernen der Pufferinitialisierung
- Rust führt aus Sicherheitsgründen bei Schreibweisen wie [0u16; LEN] automatisch ein Zeroing durch
- C (dav1d) setzt den Puffer dagegen nicht explizit auf null, sondern schreibt nur in die tatsächlich genutzten Bereiche
- In Rust wurde mit std::mem::MaybeUninit der unnötige Initialisierungsaufwand entfernt
- Die Self-Samples der Funktion cdef_filter_neon_erased sanken deutlich von 670 auf 274
- Ein weiterer großer Align16-Puffer wurde ebenfalls optimiert, indem seine Initialisierung aus der Schleife heraus nach außen verlagert wurde, sodass die Kosten nur noch einmal anfielen
- Nach der Optimierung lag der Benchmark bei etwa 72,6 Sekunden, also 1,2 Sekunden (1,5 %) schneller
Optimierung des Struct-Vergleichs
- Die Analyse der invertierten Stacks im Profiler zeigte, dass die Funktion add_temporal_candidate ineffizienter als erwartet arbeitete
- Beim Vergleich der Felder der Struct Mv innerhalb dieser Funktion erzeugte die automatisch abgeleitete PartialEq-Implementierung unnötig langsamen Code
- In C wird mit einer union ein effizienter Vergleich auf Basis von
uint32_tdurchgeführt - In Rust wurde ohne
unsafestattdessen mit dem Trait zerocopy::AsBytes ein Vergleich auf Byte-Slice-Basis implementiert - Diese Optimierung brachte nochmals 0,5 Sekunden Leistungsgewinn (rund 0,7 %)
Ergebnis und Fazit
- Mit zwei einfachen Optimierungen (Entfernen der Pufferinitialisierung, Byte-Vergleich von Structs) wurde eine Verkürzung der Laufzeit um mehr als 2 % erreicht
- Es verbleibt weiterhin ein Leistungsunterschied von etwa 6 %, es gibt also noch erhebliches Optimierungspotenzial
- Es bestätigte sich, dass der Vergleich zwischen Profiler-Snapshots eine effektive Methode ist
- Weitere Optimierungen auf Basis der Snapshot-Analyse von rav1d und dav1d erscheinen sehr wahrscheinlich
- Dank des aktiven Feedbacks und der Zusammenarbeit der Projekt-Maintainer konnten Verbesserungen umgesetzt werden, ohne die Sicherheit zu beeinträchtigen
Zusammenfassung
- Mit den Tools Profiler (samply) und Benchmarking (hyperfine) wurde der Laufzeitunterschied von 6 Sekunden (9 %) zwischen rav1d und dav1d präzise analysiert
- Zwei zentrale Optimierungen:
- Entfernen unnötigen Buffer-Zeroings in ARM-spezifischem Code (1,2 Sekunden, -1,6 %)
- Ersetzen der PartialEq-Implementierung für kleine numerische Structs durch einen schnellen Byte-Vergleich (0,5 Sekunden, -0,7 %)
- Jede Optimierung blieb ohne neuen
unsafe-Code und auf wenige Dutzend Zeilen beschränkt - Durch die Zusammenarbeit mit den Maintainern und die Prüfung der PRs wurden zugleich Zuverlässigkeit und Qualität verbessert
- Da noch eine Leistungslücke von etwa 6 % bleibt, besteht reichlich Raum für weitere profilerbasierte Vergleichsoptimierungen
Probier es aus! Vielleicht kann rav1d irgendwann sogar schneller werden als dav1d 👀🦀.
1 Kommentare
Hacker-News-Kommentare
u16ein interessantes Thema sei, dazu wurde der relevante Issue-Link geteilt: https://github.com/rust-lang/rust/issues/140167-O3sei übertrieben, bei-O2jedoch eine vernünftige Entscheidung. Wenn eine der Strukturen direkt nach der Operation verwendet werde, könne ein Versuch eines 32-Bit-Loads wegen fehlgeschlagenem Store Forwarding den Performancegewinn zunichtemachen. Zudem wurde darauf hingewiesen, dass dem Compiler in nicht-inlineten bzw. nicht-PGO-Situationen Informationen fehlen, die für die Beurteilung der Eignung der Optimierung nötig wären.aarch64gelte und es daher etwas unfair sei, sie als Gesamtzahl zu nennen; unter Berücksichtigung des Anteils von Arm/x86 sei etwa die Hälfte realistischer.dav1dnach WUFFS deutlich schwieriger wäre als das Übersetzen bzw. Aufräumen bestehenden C-Codes. Dennoch wurde ein solcher Versuch als lohnend betrachtet und als etwas, in das man auf zivilisatorischer Ebene investieren sollte.rav1d-Bounty sei, verbunden mit Zustimmung dazu, dass offenbar noch jemand ähnliche Gedanken habe.perfrecht leicht finde; außerdem habe man angenommen, dass das Zeroing-Problem schon im ersten Post diskutiert worden sei. Die zweite Optimierung sei komplexer und interessanter gewesen, aber ebenfalls vonperfin diese Richtung geführt worden. Daraus folgte der Rat, den Nutzen des Toolsperfnicht zu unterschätzen.perferkannt wurde, sondern durch differenzielles Profiling und manuelles Matching zwischen der C- und der Rust-Version. Zwar gebe es eineperf diff-Funktion, doch unterschiedliche Symbolnamen erschwerten das automatische Matching.aarch64erfolgte; aus Erfahrung wurde betont, dass Menschen mit unterschiedlichem Hintergrund Dinge, die im Rückblick „offensichtlich“ erscheinen, oft schnell erkennen können.dav1dzu antworten; mit einer Sportmetapher wurde erklärt, dass bloße Verbesserungen von Rekorden im Vergleich zu einem echten neuen Rekord weniger beeindruckend wirkten. Die eigentliche Lösung seien praktisch schnellere und innovative Ergebnisse.