世界を動かす技術を、日本語で。

rav1dビデオデコーダーの性能向上

概要

  • macOS M3チップ環境で、Rust製rav1dの高速化に取り組んだ事例を解説
  • 安全性を損なわず、バッファ初期化の最適化で約1%の性能向上を実現
  • C実装(dav1d)とのプロファイリング比較でボトルネックを特定
  • MaybeUninit活用やバッファ使い回しによる無駄なゼロクリアの削減を提案
  • 細かな改善でも着実なパフォーマンス向上が得られることを確認

Rust製rav1dのパフォーマンス改善事例:macOS M3での検証

背景とアプローチ

  • Rust製AV1デコーダrav1dは、C実装dav1dをc2rustで変換し、アセンブリ最適化関数を組み込み、安全性を高めた設計を採用すること
  • メモリ安全性を損なわずにC実装に近づける性能改善を目指すこと
  • 比較対象は、Rust版がC版より約5〜9%遅いという既存ベンチマーク結果であること
  • aarch64(M3環境)はx86_64より最適化が進んでいない可能性が高いこと
  • サンプリングプロファイラ(samply)で両実装の関数ごと性能差を分析すること

ベースライン測定

  • rav1dとdav1dを同一入力ファイル・同一スレッド数でビルド・ベンチマーク実施すること
  • hyperfineで実行時間を測定し、M3チップ上でrav1dがdav1dより約9%遅いことを確認すること
  • clang/rustcのLLVMバージョン差は許容範囲内で評価すること

プロファイリングとボトルネック特定

  • samplyでプロファイリングを行い、アセンブリ関数(cdef_filter_8x8_neon等)を「アンカー」として比較すること
  • Rust版のcdef_filter_neon_erased関数でSelfサンプル数がC版より大きいことを発見すること
  • cdef_filter_neon_erasedの内部で、[u16; 200]バッファを毎回ゼロ初期化していることが性能低下の要因であることを特定すること
  • C実装ではこのバッファは未初期化で、パディング関数の書き込み先としてのみ使用されていることを確認すること

Rustでのバッファ初期化最適化

  • Rustのstd::mem::MaybeUninitを使い、不要なゼロ初期化を回避する提案
    • let mut tmp_buf = Align16([MaybeUninit::<u16>::uninit(); TMP_LEN]);で未初期化バッファを確保すること
    • 内部関数のシグネチャもMaybeUninit対応に変更すること
    • 既存unsafeコード範囲内での変更のため、新たなunsafe追加は不要であることを確認すること
  • プロファイリングの再実行で、Selfサンプル数が大幅に減少(670→274)し、C実装に近づくことを確認すること

その他の微細な最適化

  • もう一つの大きなAlign16バッファ(lr_bak)も初期化コスト削減の対象とすること
    • ループ外で一度だけ初期化し、使い回すことで無駄なゼロクリアを減らすこと
    • C実装と同様、未初期化でも安全に使えるロジックであることを確認すること
  • この改善は小さいが、積み重ねが全体性能に寄与することを強調すること

まとめ

  • RustでC並みの性能を目指すには、メモリ初期化のコストに注意し、MaybeUninitなどの活用で無駄なゼロクリアを削減する提案
  • プロファイリングにより関数単位でボトルネックを特定し、ピンポイントで改善することが重要であること
  • 小さな最適化でも積み重ねることで、着実なパフォーマンス向上を実現できることを確認すること
  • 安全性を損なわずにRustコードの効率化を図る場合、C実装の挙動と比較しながら最適化することが有効であること

Hackerたちの意見

いい投稿だね!16ビット整数のペアを比較する非効率なコードは面白い発見だった。

ありがとう!RustやLLVMの人たちが、この最適化を可能な限りコンパイラに適用させることができるか見てみたいね。Rustはメモリ初期化に関してはかなり正確だから。

面白いミームで始まる投稿はいい投稿だってわかるよね。最近の議論に関連してるみたいだね:$20Kの報酬がRustコードの最適化に対して出されてる(memorysafety.org) | 108件のコメント | https://news.ycombinator.com/item?id=43982238

明らかに名付けの決定論のケースだね!

タイトルが投稿の内容を過小評価してるね。実際には2つの良い最適化で2.3%速くなってるよ。

1.5%の最適化がaarch64専用だから、フルの数字を主張するのはちょっと不公平だと思う。arm/x86が将来のデプロイの大半を占めると考えると、もっと1/2くらいだね。

2つのu16を比較する関連の問題は面白いね。https://github.com/rust-lang/rust/issues/140167

これが一番好きなところなんだけど、議論が「私もこの問題に直面してます」とか「いつ修正されるの?」みたいな14ページのやり取りじゃないところ。ウェブ開発者として、GitHubの問題はちょっとつまらないよね。

これってコンパイラの作成の複雑さを示してる気がする。Cコンパイラがこの問題を一般的にもっとうまく対処できるとは思えないな。

あの議論の中でストアフォワーディングについての言及がないのは意外だね。-O3のコード生成はクレイジーだけど、-O2の出力は妥当だと思う。もし構造体の一つが計算されたばかりの場合、それを32ビットの単一ロードとして読み込もうとすると、ストアフォワーディングの失敗が起きて、ロードをマージする利点が消えちゃうかも。インライン化されてない、PGOもないシナリオでは、コンパイラは最適化が適切かどうかを判断するための情報が足りないんだ。

同じ条件ならコーデックはRustよりWUFFS†にあるべきだけど、dav1dのような複雑なものをWUFFSに書き直すのはかなり大変だと思う。c2rustの翻訳をきれいにするよりも、千倍難しいって言っても信じられるよ。私たちの文明にとってはそれだけの価値があると思う。† または同等の特別目的の言語だけど、WUFFSが一番近いね。

WUFFSはコンテナファイル(Matroska、webm、mp4)のパースには良さそうだけど、動画デコーダーには全然向いてないみたい。動的メモリアロケーションがないと、動的データの扱いが難しいからね。動画コーデックは単にファイルをパースしてデータを取得するだけじゃなくて、かなりの動的状態を管理する必要があるんだ。

ハハ、ちょうど「誰かrav1dのバウンティに進展あったかな?」って考えてたところだよ。

正直、彼が最初に見つけた最適化がperfを使って分かるような明らかなものだったのはちょっと驚きだね。最初の投稿でゼロバッファの問題について話し合ったと思ってたんだけど?二つ目の最適化は確かにもっと複雑で面白かったけど、やっぱりperfに指摘されてたね。そのツールを侮っちゃダメだよ!

彼はAppleデバイスのaarch64の視点から来たんだ。違うバックグラウンドの人が「後から見れば明らか」なギャップを見つけることがよくあるよね。

私の見たところ、単なるパフォーマンスだけじゃなかったよ;CとRustのバージョン間での差分プロファイルを手動でマッチングしながらやってたみたい。(perf diffは存在するけど、異なるシンボル名間でマッチできないし、使ってる人は少ないみたい。)

この記事の2日前にバッファをゼロにしなくて済むパフォーマンスの利点についての記事を見たのは面白いね。https://news.ycombinator.com/item?id=44032680

これがffmpegのTwitterアカウントがRustに対抗する理由になってるんだね。https://x.com/ffmpeg/status/1924137645988356437?s=46

ffmpegのTwitterアカウントを読むだけで、ffmpegを使う気が失せるよ。本当に代替がないのが残念だね -- 開発者たちがすごく毒性が強いみたい。確かに、最大パフォーマンスは素晴らしいけど、パイプラインのすべての部分をコントロールできる場合だけだし、一般のユーザーから信頼できないデータを受け入れる場合、ffmpegには毎年少なくとも半ダースのリモートで悪用可能なCVEがあるからね。サンドボックスがしっかりしてるか確認した方がいいよ。https://ffmpeg.org/security.html みんなが安全で速い解決策に向けて協力する中間地点がある気がするんだけど、ここでそれぞれが主張してる立場とは違うね。

より健康的な反応は、dav1dを速くするために働くことだったかもしれないね。オリンピックの記録メトリクスを洗練させて、ボルトの100mスプリント記録を9.63秒ではなく9.64秒に遡って更新させようとしたら、誰も気にしないよ、人生を楽しめって感じだけど、実際に人々が気にするような9秒の100mスプリントを走れるなら††人間ならね。ダチョウならこれは印象的じゃないけど、全体的にダチョウはオリンピックの100mスプリントには出場してないからね。

一般的にrbultjeのベンチマークは信頼してるけど、ravidのトラッキングチケットには複数のプラットフォームでのマルチスレッドの数字があって、そんなに大きな違いは見られないね。 https://github.com/memorysafety/rav1d/issues/1294 それについての返信で説明されてる?ログインしてないから元のツイートしか見えないんだ。

これめっちゃ楽しい!rustcがtransmuteトリックを実行するのを妨げるものってあるのかな? 編集: 次の段落を読んでいれば、コメントする前に[1]について学べたのに。 [1] https://github.com/rust-lang/rust/issues/140167