1 poin oleh GN⁺ 2025-05-23 | 1 komentar | Bagikan ke WhatsApp
  • Decoder AV1 berbasis Rust rav1d sekitar 6 detik, atau 9%, lebih lambat daripada dav1d berbasis C pada input yang sama, dan dua optimisasi kecil memangkas waktu eksekusi dari 73,914 detik menjadi 72,182 detik
  • Analisis membandingkan kedua binary dalam kondisi yang sama dengan samply, lalu memakai fungsi assembly Arm bersama sebagai anchor untuk menelusuri perbedaan antara wrapper Rust dan implementasi fungsi
  • Perbaikan pertama menghindari inisialisasi nol pada buffer sementara di jalur Arm dengan MaybeUninit dan memindahkan lokasi inisialisasi lr_bak, sehingga mengurangi total runtime sekitar 1,6%
  • Perbaikan kedua mengganti perbandingan tidak efisien yang dihasilkan PartialEq default untuk struct numerik kecil dengan perbandingan berbasis as_bytes() dari zerocopy, sehingga menghemat tambahan sekitar 0,5 detik
  • Dua PR tersebut menghasilkan peningkatan total 2,3% tanpa unsafe baru, tetapi pengukuran terbatas pada macOS dengan chip M3, single-thread, dan input benchmark tertentu; masih tersisa selisih sekitar 4,2 detik dibanding dav1d

Performa acuan dan lingkungan pengukuran

  • rav1d adalah port Rust dari dav1d
    • Mengonversi dav1d dengan c2rust
    • Mengintegrasikan fungsi optimisasi assembly milik dav1d
    • Mencakup pekerjaan untuk membuat kode lebih bergaya Rust dan lebih aman
  • memorysafety.org mengadakan kontes peningkatan performa rav1d, dan pada kondisi acuan rav1d berbasis Rust sekitar 5% lebih lambat daripada dav1d berbasis C
  • Pengukuran lokal dilakukan pada MacBook Air M3, lingkungan 8 core
    • rav1d: commit a654c1e82adb2d9a33ae50d2a82a7a747102cbb6
    • rustc 1.88.0-nightly, LLVM 20.1.2
    • dav1d: 1.5.1
    • Homebrew clang 20.1.4
    • File input: Chimera-AV1-8bit-1920x1080-6736kbps.ivf
    • Opsi eksekusi: --threads 1, output ke /dev/null
  • Hasil awal hyperfine adalah rav1d 73,914 detik, dav1d 67,912 detik
    • Pada file sampel yang sama, rav1d sekitar 6 detik, 9% lebih lambat
    • Versi LLVM pada clang dan rustc hanya berbeda di versi patch

Pendekatan profiling

  • Profiling menggunakan samply
    • Kecepatan sampling default adalah 1000Hz
    • Selisih 500 sampel pada fungsi tertentu kira-kira setara dengan selisih waktu eksekusi 0,5 detik
  • Karena kedua binary serupa dan berperilaku deterministik, pendekatan membandingkan selisih sampel per fungsi efektif, alih-alih memahami ulang seluruh decoder video
  • Panggilan assembly teroptimisasi yang digunakan bersama dijadikan anchor
    • dav1d memanggil cdef_filter_8x8_neon, cdef_filter_4x4_neon, lalu masing-masing mendispatch fungsi assembly terkait
    • Di rav1d, cdef_filter_neon_erased menangani dispatch semua fungsi assembly
  • Jumlah sampel cdef_filter8_pri_sec_edged_8bpc_neon hampir sama pada kedua snapshot, sehingga mengonfirmasi arah perbandingannya tepat
  • Selisih pada cdef_filter_neon_erased dan rav1d_cdef_brow jika digabung setara dengan sekitar 1% dari total waktu eksekusi rav1d
    • Total sampel Self cdef_filter_{8x8,4x4}_neon pada dav1d sekitar 400
    • Sampel Self cdef_filter_neon_erased pada rav1d sekitar 670
    • dav1d_cdef_brow_8bpc memiliki 1790 sampel, sedangkan rav1d_cdef_brow memiliki 2350 sampel

Perbaikan 1: Menghapus inisialisasi nol pada buffer sementara

  • cdef_filter_neon_erased membuat buffer sementara sebagai Align16([0u16; TMP_LEN])
    • TMP_LEN adalah 12 * 16 + 8 = 200 pada kasus terburuk
    • Akibatnya, buffer sementara yang setara dengan [u16; 200] diisi dengan nol
  • Kode C dav1d yang bersesuaian membuat buffer stack berbentuk uint16_t tmp_buf[200] __attribute__((aligned(16))), tetapi tidak menginisialisasinya
    • Buffer ini menjadi target penulisan fungsi assembly padding
    • Setelah itu, fungsi assembly filter menggunakan nilai tersebut apa adanya
  • Pada LLVM IR rav1d, terlihat kode yang mengisi 400 byte dengan nol menggunakan llvm.memset
    • Compiler Rust tidak bisa mengetahui bahwa inisialisasi ini boleh dihapus
  • MaybeUninit digunakan untuk menghindari inisialisasi nol pada buffer sementara
    • Align16([0u16; TMP_LEN]) diubah menjadi Align16([MaybeUninit::<u16>::uninit(); TMP_LEN])
    • Signature fungsi internal disesuaikan menjadi bentuk tmp: *mut MaybeUninit<u16> dan tmp: &[MaybeUninit<u16>]
    • Karena diproses di dalam jalur kode yang sudah unsafe, tidak ada blok unsafe baru yang ditambahkan
  • Setelah perubahan, sampel Self cdef_filter_neon_erased turun dari 670 menjadi 274
    • Menjadi sedikit lebih rendah daripada total sampel Self cdef_filter_{8x8,4x4}_neon pada dav1d

Kelanjutan perbaikan 1: Mengurangi inisialisasi di dalam loop

  • Saat mencari buffer Align16 besar lainnya, ditemukan inisialisasi lr_bak di dalam rav1d_cdef_brow
    • Kode lama menginisialisasi nol lr_bak setiap kali di dalam loop
    • Kode dav1d yang bersesuaian tidak menginisialisasi buffer ini
  • Di sini konversi ke MaybeUninit lebih sulit, sehingga pembuatan lr_bak dipindahkan ke luar loop
    • Inisialisasi tidak lagi dilakukan pada setiap iterasi, melainkan hanya sekali
    • Penghematannya kecil, tetapi mengurangi pekerjaan tidak perlu dari jenis yang sama
  • Pada benchmark keseluruhan yang mencakup perubahan ini, rav1d mencatat 72,644 detik
    • Peningkatan 1,2 detik dari 73,914 detik sebelumnya
    • Peningkatan sekitar 1,5% terhadap total runtime
    • Masih ada selisih dibanding 67,912 detik milik dav1d

Perbaikan 2: Mengoptimalkan perbandingan kesetaraan struktur kecil

  • Saat melakukan profiling ulang dengan tampilan inverted stack, terlihat selisih mencolok pada add_temporal_candidate
    • Selisih antara versi Rust dan C sekitar 400 sampel, setara sekitar 0,5 detik
    • Fungsi itu sendiri terdiri dari sekitar 50 baris if, for, dan panggilan utilitas singkat
  • Build ulang dilakukan dengan profil release-with-debug untuk memeriksa distribusi sampel per baris
    • if cand.mv.mv[0] == mv {
    • if cand.mv == mvp {
    • Kedua baris tersebut bersama-sama memakan sekitar 600 sampel
  • Mv di Rust adalah struktur kecil yang menggunakan #[derive(PartialEq)]
    • #[repr(C)]
    • y: i16, x: i16
  • mv pada dav1d didefinisikan sebagai union
    • struct { int16_t y, x; }
    • uint32_t n
    • Saat membandingkan, ia dibandingkan sebagai nilai 32-bit seperti mvstack[n].mv.n == mvp.n
  • Jika menggunakan union di Rust, akses field menjadi unsafe dan dapat memengaruhi seluruh lokasi penggunaan Mv
    • Sebagai gantinya, digunakan AsBytes dari zerocopy untuk membandingkan representasi byte
    • Di impl PartialEq for Mv, digunakan self.as_bytes() == other.as_bytes()
    • Pemeriksaan dengan Godbolt menunjukkan bahwa cara ini menghasilkan assembly teroptimisasi yang sama dengan pendekatan berbasis transmute
  • Optimisasi serupa juga diterapkan pada RefMvs{Mv,Ref}Pair
    • Hasil benchmark adalah 72,182 detik
    • Peningkatan sekitar 0,5 detik dibanding hasil sebelumnya, 72,644 detik
    • Peningkatan 2,3% dibanding acuan awal 73,914 detik

PartialEq default Rust dan batasan code generation

  • Alasan PartialEq default untuk struktur kecil menghasilkan code generation yang tidak efisien terkait dengan issue Rust #140167
  • Di C, struct { int16_t y, x; } bisa berada dalam keadaan ketika hanya y yang diinisialisasi dan x belum diinisialisasi
    • Jika perbandingannya adalah this.y == other.y && this.x == other.x dan semua y berbeda, x tidak perlu dibaca
    • Dengan mempertimbangkan kasus seperti ini, optimisasi menjadi satu load memori hanya valid jika ada jaminan bahwa semua field selalu terinisialisasi
  • Diskusi terkait membahas bahwa LLVM tidak memiliki cara untuk mengekspresikan properti “load melalui pointer ini selalu membaca byte yang sudah terinisialisasi”
  • zerocopy dapat memeriksa secara statis kondisi aman yang membolehkan struktur direpresentasikan sebagai slice byte, sehingga perbandingan teroptimisasi bisa diimplementasikan tanpa unsafe baru

Hasil akhir dan sisa selisih performa

  • PR pertama menghindari inisialisasi nol yang mahal pada hot path khusus Arm
    • PR #1397
    • Peningkatan waktu eksekusi 1,2 detik
    • Sekitar -1,6%
  • PR kedua mengganti implementasi PartialEq default pada struktur numerik kecil dengan perbandingan berbasis byte
    • PR #1400
    • Peningkatan waktu eksekusi 0,5 detik
    • Sekitar -0,7%
  • Kedua perubahan jika digabung hanya berukuran puluhan baris dan tidak memperkenalkan unsafe baru ke codebase
  • Waktu eksekusi akhir rav1d adalah 72,182 detik, 2,3% lebih cepat daripada titik awal
    • Masih ada selisih sekitar 4,2 detik dibanding 67,912 detik milik dav1d
    • Mengurangi sekitar 30% dari selisih performa yang diamati di awal
  • Masih tersisa selisih sekitar 6% antara kedua implementasi, dan perbandingan snapshot profiler dav1d dan rav1d masih dapat terus dimanfaatkan untuk mencari optimisasi tambahan

1 komentar

 
GN⁺ 2025-05-23
Opini Hacker News
  • Isu terkait membandingkan dua u16 ini menarik
    https://github.com/rust-lang/rust/issues/140167

    • Agak mengejutkan bahwa store forwarding tidak disebut dalam diskusi itu
      Pembuatan kode -O3 memang aneh, tetapi output -O2 masuk akal. Jika sebuah struct baru saja dihitung, mencoba membacanya sebagai satu load 32-bit bisa menyebabkan kegagalan store forwarding, sehingga manfaat penggabungan load bisa hilang. Dalam situasi tanpa inlining dan tanpa PGO, compiler kekurangan informasi untuk menilai apakah optimisasi itu tepat
    • Bagus bahwa diskusi ini bukan 14 halaman komentar seperti “saya juga kena masalah ini” atau “kapan diperbaiki?”
      Sebagai web developer, issue GitHub sering kali terasa cukup buruk
    • Kasus ini tampaknya menunjukkan kompleksitas penulisan compiler
      Sulit juga untuk yakin bahwa compiler C bisa menangani masalah ini dengan lebih baik pada kasus umum
  • Hal-hal seperti ini tampaknya membuat akun Twitter ffmpeg mengambil sikap anti-Rust
    https://x.com/ffmpeg/status/1924137645988356437?s=46

    • Biasanya benchmark rbultje bisa dipercaya, tetapi issue pelacakan rav1d memiliki angka multi-thread dari berbagai platform dan tidak menunjukkan selisih sebesar itu
      https://github.com/memorysafety/rav1d/issues/1294
      Karena tidak login, saya hanya bisa melihat tweet aslinya; saya penasaran apakah ada penjelasan di balasannya
    • Membaca akun Twitter ffmpeg saja sudah membuat saya enggan memakai ffmpeg
      Sayang tidak ada alternatif yang layak, dan para developernya terlihat cukup agresif. Jika kita mengendalikan seluruh pipeline, performa terbaik tentu bagus, tetapi jika menerima data tak tepercaya dari pengguna yang tidak dikenal, ffmpeg setiap tahun memiliki setidaknya lima atau enam CVE yang bisa dieksploitasi dari jarak jauh. Sebaiknya sandbox-nya dibuat kuat
      https://ffmpeg.org/security.html
      Alih-alih posisi yang masing-masing pihak pertahankan di sini, rasanya ada titik tengah tempat semua orang bergerak menuju solusi yang aman sekaligus cepat
    • Respons yang lebih sehat mungkin adalah bekerja membuat dav1d lebih cepat
      Tidak ada yang peduli jika kriteria penghitungan rekor Olimpiade disempurnakan lalu rekor 100m Bolt direvisi mundur dari 9,63 detik menjadi 9,64 detik. Tetapi jika seseorang benar-benar berlari 100m dalam 9 detik, orang akan memperhatikan. Tentu saja itu kalau ia manusia; kalau burung unta, tidak terlalu mengesankan, tetapi pada umumnya burung unta tidak ikut nomor 100m Olimpiade
  • Menarik bahwa tulisan tentang keuntungan performa dari tidak menginisialisasi buffer dengan nol muncul dua hari setelah tulisan ini
    https://news.ycombinator.com/item?id=44032680

  • Judulnya meremehkan isi tulisan
    Sebenarnya, dengan dua optimisasi bagus, performanya menjadi 2,3% lebih cepat

    • Optimisasi 1,5% itu khusus aarch64, jadi agak tidak adil jika mengklaim angka keseluruhannya begitu saja
      Jika Arm dan x86 akan menjadi mayoritas distribusi ke depan, mungkin lebih tepat menganggapnya sekitar separuh
  • Tulisan yang bagus, dan bagian ditemukannya kode yang tidak efisien pada perbandingan pasangan integer 16-bit terasa menarik

    • Saya penasaran apakah pihak Rust/LLVM bisa meningkatkan compiler agar menerapkan optimisasi ini kapan pun memungkinkan
      Rust bisa memiliki informasi yang jauh lebih akurat tentang apakah memori sudah diinisialisasi atau belum
  • Jika kondisinya sama, menurut saya codec seharusnya ditulis dengan WUFFS, bukan Rust
    Namun, menulis ulang sesuatu serumit dav1d dengan WUFFS bisa jauh lebih besar pekerjaannya daripada merapikan hasil konversi c2rust. Saya bisa percaya kalau itu seribu kali lebih sulit. Tetap saja, untuk peradaban secara keseluruhan, menurut saya itu layak dicoba
    Yang saya maksud adalah WUFFS atau bahasa khusus setara, dan WUFFS sudah ada

    • WUFFS mungkin sangat bagus untuk parsing file kontainer seperti Matroska, webm, mp4, tetapi sama sekali tidak terlihat cocok untuk decoder video
      Tanpa alokasi memori dinamis, data dinamis sulit ditangani. Codec video bukan sekadar mem-parsing file untuk mendapatkan data; ia harus mengelola banyak state yang cukup dinamis
  • Kalau sebuah tulisan diawali meme lucu, kita tahu itu tulisan bagus
    Tampaknya juga terkait dengan diskusi terbaru: $20K Bounty Offered for Optimizing Rust Code in Rav1d AV1 Decoder (memorysafety.org) | 108 comments | https://news.ycombinator.com/item?id=43982238

  • Jujur, saya agak terkejut optimisasi pertama adalah sesuatu yang cukup jelas terlihat hanya dengan perf
    Sepertinya pada tulisan pertama masalah inisialisasi buffer ke nol sudah dibahas, sedangkan optimisasi kedua jelas lebih rumit dan menarik, tetapi tetap saja perf menunjukkannya. Alat ini tidak boleh diremehkan

    • Kelihatannya bukan sekadar memakai perf saja, melainkan lebih seperti melakukan profiling diferensial antara versi C dan versi Rust lalu mencocokkannya secara manual
      Memang ada perf diff, tetapi ia tidak bisa mencocokkan nama simbol yang berbeda, dan tampaknya tidak banyak orang yang memakainya
    • Sepertinya ini bisa dilakukan karena pendekatannya dari sudut pandang aarch64 di perangkat Apple
      Saya sering melihat orang dari latar belakang berbeda menemukan celah yang “jelas jika dilihat kembali”
  • Ini benar-benar menarik
    Saya sempat bertanya-tanya apakah ada yang mencegah rustc melakukan trik transmute itu, tetapi kalau saja saya membaca paragraf berikutnya, saya akan tahu issue ini sebelum berkomentar
    https://github.com/rust-lang/rust/issues/140167