1 puan yazan GN⁺ 2025-05-23 | 1 yorum | WhatsApp'ta paylaş
  • Rust tabanlı AV1 çözücü rav1d, aynı girdide C tabanlı dav1d'den yaklaşık 6 saniye, %9 daha yavaştı; iki küçük optimizasyonla çalışma süresi 73.914 saniyeden 72.182 saniyeye düştü
  • Analiz, iki ikiliyi aynı koşullarda samply ile karşılaştırdı ve ortak Arm assembly fonksiyonlarını çapa alarak Rust sarmalayıcısı ile fonksiyon uygulaması arasındaki farkları izledi
  • İlk iyileştirme, Arm yolundaki geçici tamponun 0 ile başlatılmasını MaybeUninit ile önleyip lr_bak başlatma konumunu taşıyarak toplam çalışma süresini yaklaşık %1,6 azalttı
  • İkinci iyileştirme, küçük sayısal struct için varsayılan PartialEq'in ürettiği verimsiz karşılaştırmayı zerocopy'nin as_bytes() tabanlı karşılaştırmasıyla değiştirerek ek olarak yaklaşık 0,5 saniye kazandırdı
  • İki PR, yeni unsafe eklemeden toplamda %2,3 iyileşme sağladı; ancak ölçümler macOS M3 çip, tek iş parçacığı ve belirli bir benchmark girdisiyle sınırlıydı, ayrıca dav1d ile hâlâ yaklaşık 4,2 saniyelik fark kaldı

Referans performans ve ölçüm ortamı

  • rav1d, dav1d'nin Rust portudur
    • c2rust ile dav1d dönüştürüldü
    • dav1d'nin assembly ile optimize edilmiş fonksiyonları entegre edildi
    • Kodun daha Rust tarzı ve güvenli hâle getirilmesi de buna dahil
  • memorysafety.org, rav1d performans iyileştirme yarışması düzenledi ve başlangıç durumunda Rust tabanlı rav1d, C tabanlı dav1d'den yaklaşık %5 daha yavaştı
  • Yerel ölçümler MacBook Air M3, 8 çekirdekli ortamda yapıldı
    • rav1d: commit a654c1e82adb2d9a33ae50d2a82a7a747102cbb6
    • rustc 1.88.0-nightly, LLVM 20.1.2
    • dav1d: 1.5.1
    • Homebrew clang 20.1.4
    • Girdi dosyası: Chimera-AV1-8bit-1920x1080-6736kbps.ivf
    • Çalıştırma seçenekleri: --threads 1, çıktı /dev/null
  • İlk hyperfine sonucu rav1d 73.914 saniye, dav1d 67.912 saniye oldu
    • Aynı örnek dosyada rav1d yaklaşık 6 saniye, %9 daha yavaştı
    • clang ile rustc'nin LLVM sürümleri yalnızca patch sürümünde farklıydı

Profil çıkarma yaklaşımı

  • Profil çıkarma için samply kullanıldı
    • Varsayılan örnekleme hızı 1000Hz
    • Belirli bir fonksiyondaki 500 örnek farkı kabaca 0,5 saniyelik çalışma süresi farkına karşılık geliyor
  • İki ikili benzer ve deterministik davrandığı için, tüm video çözücüyü baştan anlamak yerine fonksiyon bazında örnek farklarını karşılaştırmak etkili oldu
  • Ortak kullanılan optimize assembly çağrıları çapa olarak alındı
    • dav1d, cdef_filter_8x8_neon ve cdef_filter_4x4_neon çağırıyor, bunlar da ilgili assembly fonksiyonlarına dispatch ediyor
    • rav1d ise tüm assembly fonksiyonu dispatch işini cdef_filter_neon_erased ile yapıyor
  • cdef_filter8_pri_sec_edged_8bpc_neon için örnek sayısı iki anlık görüntüde de neredeyse aynıydı; bu da karşılaştırma yönünün doğru olduğunu gösterdi
  • cdef_filter_neon_erased ile rav1d_cdef_brow farkı birlikte, rav1d'nin toplam çalışma süresinin yaklaşık %1'ine karşılık geldi
    • dav1d'de cdef_filter_{8x8,4x4}_neon için Self örneklerinin toplamı yaklaşık 400
    • rav1d'de cdef_filter_neon_erased için Self örnekleri yaklaşık 670
    • dav1d_cdef_brow_8bpc 1790 örnek alırken rav1d_cdef_brow 2350 örnek aldı

İyileştirme 1: geçici tamponun 0 ile başlatılmasını kaldırmak

  • cdef_filter_neon_erased, geçici tamponu Align16([0u16; TMP_LEN]) ile oluşturuyor
    • TMP_LEN en kötü durumda 12 * 16 + 8 = 200
    • Sonuç olarak [u16; 200] karşılığı geçici tampon 0 ile dolduruluyor
  • Buna karşılık gelen dav1d C kodu, uint16_t tmp_buf[200] __attribute__((aligned(16))) biçiminde bir stack tamponu oluşturuyor ama başlatmıyor
    • Bu tampon, padding assembly fonksiyonunun yazma hedefi oluyor
    • Ardından filter assembly fonksiyonu bu değerleri doğrudan kullanıyor
  • rav1d'nin LLVM IR çıktısında 400 baytı 0 ile dolduran llvm.memset kodu görünüyor
    • Rust derleyicisi bu başlatmanın kaldırılmasının güvenli olduğunu bilemiyordu
  • Geçici tamponun 0 ile başlatılmasını önlemek için MaybeUninit kullanıldı
    • Align16([0u16; TMP_LEN]), Align16([MaybeUninit::<u16>::uninit(); TMP_LEN]) olarak değiştirildi
    • İç fonksiyon imzaları tmp: *mut MaybeUninit<u16>, tmp: &[MaybeUninit<u16>] biçimine uyarlandı
    • Zaten unsafe olan kod yolunda işlendiği için yeni bir unsafe bloğu eklenmedi
  • Değişiklikten sonra cdef_filter_neon_erased için Self örnekleri 670'ten 274'e düştü
    • Bu değer, dav1d'deki cdef_filter_{8x8,4x4}_neon Self örnekleri toplamının biraz altına indi

İyileştirme 1'in uzantısı: döngü içi başlatmayı azaltmak

  • Daha büyük Align16 tamponları aranırken rav1d_cdef_brow içindeki lr_bak başlatması bulundu
    • Eski kod, döngü içinde lr_bak'ı her seferinde 0 ile başlatıyordu
    • Karşılık gelen dav1d kodu bu tamponu başlatmıyor
  • Burada MaybeUninit dönüşümü daha zordu, bu yüzden lr_bak oluşturma işlemi döngü dışına taşındı
    • Böylece her yinelemede başlatmak yerine yalnızca bir kez başlatıldı
    • Kazanç küçük olsa da aynı tür gereksiz işi azaltmış oldu
  • Bu değişiklik de dahil edildiğinde tam benchmark'ta rav1d 72.644 saniye ölçüldü
    • Önceki 73.914 saniyeye göre 1,2 saniye iyileşme
    • Toplam çalışma süresine göre yaklaşık %1,5 iyileşme
    • dav1d'nin 67.912 saniyelik sonucuyla arasında hâlâ fark var

İyileştirme 2: küçük struct eşitlik karşılaştırmasını optimize etmek

  • Ters çevrilmiş stack görünümünde yeniden profil çıkarılınca add_temporal_candidate içinde belirgin bir fark görüldü
    • Rust ve C sürümleri arasındaki fark yaklaşık 400 örnekti; bu da yaklaşık 0,5 saniyeye denk geliyor
    • Fonksiyonun kendisi yaklaşık 50 satırlık if, for ve kısa yardımcı çağrılardan oluşuyor
  • Satır bazında örnek dağılımını görmek için yeniden release-with-debug profiliyle derlendi
    • if cand.mv.mv[0] == mv {
    • if cand.mv == mvp {
    • Bu iki satır birlikte yaklaşık 600 örnek tuttu
  • Rust'taki Mv, #[derive(PartialEq)] kullanan küçük bir struct
    • #[repr(C)]
    • y: i16, x: i16
  • dav1d'deki mv ise union olarak tanımlanmış
    • struct { int16_t y, x; }
    • uint32_t n
    • Karşılaştırma mvstack[n].mv.n == mvp.n şeklinde 32 bitlik değer üzerinden yapılıyor
  • Rust'ta union kullanılırsa alan erişimi unsafe oluyor ve bu durum Mv kullanımının tamamını etkileyebiliyor
    • Bunun yerine zerocopy'nin AsBytes özelliğiyle bayt gösterimi karşılaştırıldı
    • impl PartialEq for Mv içinde self.as_bytes() == other.as_bytes() kullanıldı
    • Godbolt kontrolü, bunun transmute tabanlı yaklaşımla aynı optimize assembly çıktısını ürettiğini gösterdi
  • Benzer optimizasyon RefMvs{Mv,Ref}Pair için de uygulandı
    • Benchmark sonucu 72.182 saniye oldu
    • Önceki 72.644 saniyeye göre yaklaşık 0,5 saniye iyileşme
    • İlk referans olan 73.914 saniyeye göre %2,3 iyileşme

Rust varsayılan PartialEq ve kod üretim sınırları

  • Küçük struct'lar için varsayılan PartialEq'in verimsiz kod üretmesine yol açmasının nedeni Rust sorunu #140167 ile bağlantılı
  • C'de struct { int16_t y, x; }, yalnızca y başlatılmışken x başlatılmamış durumda olabilir
    • Karşılaştırma this.y == other.y && this.x == other.x ise ve tüm y değerleri farklıysa, x'i okumaya gerek kalmaz
    • Bu tür durumlar düşünüldüğünde, tek bir bellek yüklemesiyle optimize etmek ancak tüm alanların her zaman başlatıldığı garanti edildiğinde geçerlidir
  • İlgili tartışmalarda, LLVM'nin “bu işaretçi üzerinden yapılan yükleme her zaman başlatılmış baytları okur” özelliğini ifade etme yoluna sahip olmaması ele alınıyor
  • zerocopy, struct'ların bayt dilimi olarak temsil edilmesinin güvenlik koşullarını statik olarak doğrulayabildiği için, yeni unsafe olmadan optimize edilmiş karşılaştırma uygulanabildi

Nihai sonuçlar ve kalan performans farkı

  • İlk PR, Arm'a özel sıcak yoldaki pahalı 0 ile başlatmayı ortadan kaldırdı
    • PR #1397
    • Çalışma süresinde 1,2 saniye iyileşme
    • Yaklaşık -%1,6
  • İkinci PR, küçük sayısal struct'lar için varsayılan PartialEq uygulamasını bayt tabanlı karşılaştırmayla değiştirdi
    • PR #1400
    • Çalışma süresinde 0,5 saniye iyileşme
    • Yaklaşık -%0,7
  • Bu iki değişiklik birlikte birkaç düzine satır ölçeğinde kaldı ve kod tabanına yeni unsafe eklemedi
  • Nihai rav1d çalışma süresi 72.182 saniye oldu; bu da başlangıca göre %2,3 daha hızlı
    • dav1d'nin 67.912 saniyelik sonucuna göre yaklaşık 4,2 saniye fark var
    • Başlangıçta gözlenen performans farkının yaklaşık %30'u kapatıldı
  • İki uygulama arasında hâlâ yaklaşık %6 fark bulunuyor ve dav1d ile rav1d profiler anlık görüntülerinin karşılaştırılması, ek optimizasyonları aramak için kullanılmaya devam edilebilir

1 yorum

 
GN⁺ 2025-05-23
Hacker News yorumları
  • İki u16’yı karşılaştırma ile ilgili mesele ilginç
    https://github.com/rust-lang/rust/issues/140167

    • O tartışmada store forwarding konusundan bahsedilmemiş olması şaşırtıcı
      -O3 kod üretimi tuhaf, ama -O2 çıktısı makul. Bir struct az önce hesaplanmış durumdaysa, onu tek bir 32 bit load ile okumaya çalışmak store forwarding başarısızlığına yol açıp load birleştirmenin faydasını ortadan kaldırabilir. Inline edilmediği ve PGO olmadığı durumda derleyicinin bu optimizasyonun uygun olup olmadığını değerlendirecek yeterli bilgisi yok
    • Bu tartışmanın “bende de bu sorun var”, “ne zaman düzeltilecek?” gibi 14 sayfa yorumdan oluşmaması güzel
      Bir web geliştiricisi olarak GitHub issue’ları çoğu zaman epey kötü buluyorum
    • Bu örnek derleyici yazmanın karmaşıklığını gösteriyor gibi
      C derleyicilerinin de genel durumda bu sorunu daha iyi ele alabileceğinden emin olmak zor
  • Bu tür şeyler yüzünden ffmpeg Twitter hesabı Rust’a karşı bir tutum sergiliyor gibi
    https://x.com/ffmpeg/status/1924137645988356437?s=46

    • Genelde rbultje’nin benchmark’larına güvenirim, ama rav1d takip issue’sunda birden fazla platforma ait çok iş parçacıklı sayılar var ve o kadar büyük bir fark göstermiyorlar
      https://github.com/memorysafety/rav1d/issues/1294
      Giriş yapmadığım için yalnızca asıl tweet’i görebiliyorum; yanıtlarında açıklanan bir kısım var mı merak ediyorum
    • Sırf ffmpeg Twitter hesabını okumak bile ffmpeg kullanma isteğini kaçıracak düzeyde
      Düzgün bir alternatif olmaması üzücü ve geliştiriciler epey saldırgan görünüyor. Tüm pipeline’ı kontrol ediyorsanız en iyi performans iyi olabilir, ama belirsiz kullanıcılardan güvenilmeyen veri alıyorsanız ffmpeg’de her yıl uzaktan istismar edilebilir en az beş altı CVE çıkıyor. Sandbox’ı sağlam tutmakta fayda var
      https://ffmpeg.org/security.html
      Burada herkesin tutunduğu konumlardan ziyade, herkesin güvenli ve hızlı bir çözüme doğru gittiği bir orta nokta var gibi
    • Daha sağlıklı tepki, dav1d’yi daha hızlı yapmak için çalışmak olurdu
      Olimpiyat rekoru hesaplama kriterlerini ince ayarlayıp Bolt’un 100 m rekorunu 9,63 saniye değil 9,64 saniye olarak geriye dönük düzeltsek kimsenin umurunda olmaz. Ama gerçekten 100 metreyi 9 saniyede koşarsanız ilgi görürsünüz. Tabii insan olduğunuzda; devekuşuysanız etkileyici olmaz, ama genelde devekuşları Olimpiyat 100 m yarışına katılmaz
  • Buffer’ı sıfırla başlatmayarak elde edilen performans avantajıyla ilgili bir yazının, bu yazı çıktıktan iki gün sonra görülmesi ilginç
    https://news.ycombinator.com/item?id=44032680

  • Başlık yazının değerini düşük gösteriyor
    Aslında iki iyi optimizasyon sayesinde %2,3 daha hızlı hale geliyor

    • %1,5’lik optimizasyon aarch64’e özel olduğu için toplam sayıyı aynen iddia etmek biraz haksız görünüyor
      Arm ve x86’ın gelecekte dağıtımların çoğunu oluşturacağını düşünürsek bunu yarı yarıya saymak daha doğru olur gibi
  • Güzel bir yazı; 16 bit tamsayı çiftlerini karşılaştırma sırasında verimsiz kod bulunması ilginçti

    • Rust/LLVM tarafında, mümkün olduğunda bu optimizasyonu uygulayacak şekilde derleyicinin iyileştirilip iyileştirilemeyeceğini merak ediyorum
      Rust, belleğin ilklendirilip ilklendirilmediği konusunda çok daha doğru bilgiye sahip olabilir
  • Koşullar eşitse codec’lerin Rust yerine WUFFS ile yazılması gerektiğini düşünüyorum
    Yine de dav1d kadar karmaşık bir şeyi WUFFS ile baştan yazmak, c2rust dönüşüm sonucunu temizlemekten çok daha büyük bir iş olabilir. Bin kat daha zor deseler inanırım. Yine de tüm medeniyet ölçeğinde denemeye değer olduğunu düşünüyorum
    WUFFS’ten ya da eşdeğer özel amaçlı bir dilden bahsediyorum; WUFFS zaten var

    • WUFFS, Matroska, webm, mp4 gibi container dosyalarını parse etmek için harika olurdu, ama video decoder için hiç uygun görünmüyor
      Dinamik bellek ayırma yoksa dinamik verilerle uğraşmak zor. Video codec’leri yalnızca bir dosyayı parse edip veri elde etmekten ibaret değil; oldukça dinamik pek çok durumu yönetmeleri gerekiyor
  • Komik bir meme ile başlayan bir yazıysa iyi yazı olduğunu anlarsınız
    Yakın zamandaki tartışmayla da ilgili görünüyor: $20K Bounty Offered for Optimizing Rust Code in Rav1d AV1 Decoder (memorysafety.org) | 108 comments | https://news.ycombinator.com/item?id=43982238

  • Açıkçası ilk optimizasyonun sadece perf kullanılsa bile epey net görülebilen bir şey olması beni biraz şaşırttı
    İlk yazıda buffer’ı sıfırla başlatma sorununu zaten tartışmışlardı sanırım; ikinci optimizasyon kesinlikle daha karmaşık ve ilginçti ama yine de perf ona işaret etti. Bu aracı küçümsememek gerek

    • Görünüşe göre yalnızca perf kullanmak değil, C sürümü ile Rust sürümü arasında diferansiyel profilleme yapıp bunları elle eşleştirmek söz konusu
      perf diff var, ama farklı sembol adlarını eşleştiremiyor ve kullanan da pek fazla görünmüyor
    • Apple cihazlarında aarch64 perspektifiyle yaklaştığı için mümkün olmuş gibi
      Farklı arka planlardan gelen insanların “geriye dönüp bakınca bariz” olan açıkları bulduğunu sık sık görüyorum
  • Bu gerçekten çok eğlenceli
    rustc’nin o transmute hilesini yapmasını engelleyen bir şey var mı diye merak etmiştim; bir sonraki paragrafı okusaydım yorum yazmadan önce bu issue’dan haberdar olurdum
    https://github.com/rust-lang/rust/issues/140167