write で Linux パイプにデータを書き込むと、単純なメモリ書き込みよりはるかに遅くなり、実験環境では メモリバッファ書き込み 167GB/s に対して パイプ書き込み 17GB/s 程度にとどまった
- ボトルネックは単一のデータコピーだけでは説明できず、
pipe_write 内の ページ割り当て、ロック、カーネルのコピー処理ルーチンのコストが積み重なっている
vmsplice はユーザー空間バッファをカーネルへコピーせずにパイプへ接続し、__alloc_pages、_raw_spin_lock_irq、copy_user_enhanced_fast_string のような高コスト経路を回避する
- Fizz Buzz スループット事例では、
vmsplice を使う解法は単一コアで 60.8GiB/s、複数コアで 208.3GiB/s に達し、別実験の vmsplice 例では 210GB/s を記録した
- 訂正事項により SIMD未使用ペナルティ の解釈は信頼しにくく、プロセス間通信は L1 キャッシュだけでは完結しないため、167GB/s を実際のパイプ処理量として期待するのは難しい
出発点: vmsplice が生む大きな差
- 一部のプログラムは、パイプ経由でデータをより高速に移動させるために
vmsplice システムコールを使う
- Code Golf StackExchange の Fizz Buzz スループット競争では、解法は大きく 2 グループに分かれる
vmsplice を使わない解法は毎秒数 GiB 程度で、neil の解法は 8.4GiB/s に達した
vmsplice を使う解法は tkluck の 15.5GiB/s、ais523 の 60.8GiB/s、david の複数コア 208.3GiB/s まで伸びる
- 単にカーネル空間とユーザー空間の間のコピーを減らす効果だけでは、単一コアで約 7 倍の差を説明しにくい
- 独自実験でも、ais523 の解法は 96.4GiB/s、david の解法は 7 コア使用時 277GB/s、1 コアあたり約 40GB/s を記録した
基準線: ユーザー空間メモリ書き込み
- システムコールなしでユーザー空間メモリ内の 32KiB バッファを繰り返しコピーする Rust プログラムは、実験環境で 167GB/s を記録した
- この数値は、使用した CPU の L1 キャッシュ書き込み速度と一致するレベルと見なされる
- 実験システムは Ryzen 9 7950X3D、DDR5 6000T/s、Debian 12、Linux 6.1.0-18-amd64 環境
- CPU 緩和オプションは
mitigations=off で無効化されている
ftrace プロファイリングでは、ほとんどの時間が __memset_avx512_unaligned_erms に費やされていたことが示された
- ただし、訂正事項によりこの解釈には制限がある
- 中断時点の命令は
rep stos であり、これは AVX-512 命令ではない
- AVX2 と SSE2 のみを使うよう制限してもスループットが 167GB/s のままだったのも、どの場合も
rep stos が使われていたためである
write でパイプに書くときの実際のコスト
- 同じサイズのバッファを
stdout.write() でパイプに書き込み、pv >/dev/null で消費すると、スループットは 17GB/s まで低下した
- プロファイリング結果では、ほとんどの時間が
write システムコール内で消費され、とくに 95% が pipe_write 内にとどまっていた
pipe_write 内では、ページ準備、ロック、コピーがそろってコストになる
__alloc_pages: 全体時間の 36%、パイプ用の新しいメモリページを準備する
__mutex_lock.constprop.0: 全体時間の 25%、パイプ書き込みのためのロックコストを占める
_raw_spin_lock_irq: 全体時間の 5%、パイプ書き込み関連のロックコストとして現れる
copy_user_enhanced_fast_string: 全体時間の約 20%、ユーザー空間からカーネル側へデータをコピーする
pv が splice でページを /dev/null へ移動して消費するため、同じ少数のページをループで再利用し続けるのは難しい
カーネルのコピー処理ルーチンと訂正後の解釈
copy_user_enhanced_fast_string を逆アセンブルすると、大きなバッファコピーは REP MOV 命令で処理されている
- この関数は C ではなく Linux カーネルのアセンブリコードで実装されており、コンパイラ最適化漏れではなく意図された経路である
- 元の実験では、ユーザー空間で
rep movsb を直接呼び出して 80GB/s を得ており、これをカーネルのコピー処理ルーチンが約 2 倍遅いという解釈につなげていた
- その後の訂正で、実験条件が異なることが判明した
- 32KiB バッファ 2 個で L1 データキャッシュが飽和する
- 16KiB バッファを使うと性能は 153GB/s まで上がる
- したがって、カーネルコピーでベクトル命令を使わないことが大きなペナルティだという元の解釈は信頼しにくい
- それでも、パイプ書き込みで メモリ管理オーバーヘッド が大きい点は残る
vmsplice が避けるカーネル経路
vmsplice はユーザー空間のバッファ全体をカーネルへコピーせずにパイプへ渡し、パイプ利用コストを下げる
- Francesco の
pipes-speed-test に含まれる ./write 例は、無限に 'X' を書く最小例として使われる
- この例は 210GB/s を記録したが、同じバッファを繰り返し
vmsplice に渡しているため、一般的なデータ生成処理とは異なる
- 定数バイト列でなければ、新しいデータをバッファへ埋める必要がある
- このとき、単純なメモリ書き込み上限である 167GB/s が再び関係してくる
vmsplice 経路でも __mutex_lock.constprop.0 に 37% の時間が費やされる
- しかし、
write 経路で見られた __alloc_pages、_raw_spin_lock_irq、copy_user_enhanced_fast_string は現れない
- 代わりに
add_to_pipe、import_iovec、iov_iter_get_pages2 が主要経路として現れ、vmsplice が write の高コスト部分を回避していることを示す
残る結論と注意点
- 実験上、Linux パイプに
write で書き込む経路は、単純なメモリ書き込みより約 10 倍遅い
- 元の結論は、パイプ書き込みではロックコストと SIMD コンテキスト保存・復元コストが大きく、
splice と vmsplice がそれを回避するという内容だった
- 訂正後は、結論をより限定的に見るべきである
- カーネルの メモリ管理オーバーヘッド は依然としてパイプ性能低下の重要な要因である
- ベクトル命令未使用が予想ほど大きなペナルティだという解釈は不正確である
- プロセス間通信は L1 キャッシュだけで成り立つわけではないため、167GB/s は実際のパイプ読み取りまで含めたスループットとしては達成しにくい
- いくつかの重大な誤りが訂正されており、結果の信頼性には限界があるため、数値は方向性を見るためのものとして解釈すべきである
- パイプのスループットを高めるには、システムコール数を減らすだけでなく、
write が通るカーネル内部経路やバッファ処理方式まで合わせて見る必要がある
1件のコメント
Hacker News の意見
この問題に取り組もうとしているサイドプロジェクトがある: https://lwn.net/Articles/976836/
アイデアは、対応するすべてのファイルディスクリプタに対してリングバッファを取得するシステムコールを作るというもの。パイプも含まれ、両端がリングバッファの利用に対応していれば同じリングバッファをマップしてゼロコピー I/Oを行い、場合によってはカーネル呼び出しをまったく行わずに済むかもしれない。一緒にやる人を探している
リングバッファが満杯または空になったときに sleep/poll でき、それ以外ではロックなし、システムコールのオーバーヘッドもなしで動作する
ringbuffer_wait()もpoll()で読み取り可能状態にして通知できるのではないか瞬間的な hiccup だけでも、十分なバッファリングがなければパイプが完全におかしくなり、必要なバッファサイズはシステムごとに異なる
JMPがなぜ単なるRETではないのかは、CONFIG_RETHUNK オプションのため。objdumpの逆アセンブルで見えているのは、RETがJMP __x86_return_thunkに置き換えられた結果だhttps://github.com/torvalds/linux/blob/v6.1/arch/x86/include...
https://github.com/torvalds/linux/blob/v6.1/arch/x86/lib/ret...
関数の先頭と末尾の NOP 命令は ftrace 用ではなく、ASM_CLAC/ASM_STAC マクロ由来のもの。このマクロは X86_FEATURE_SMAP が検出されると、実行時に CLAC/STAC 命令で埋めるための空間を残す。どちらの命令も 3 バイトなので NOP の数と同じになる
https://github.com/torvalds/linux/blob/v6.1/arch/x86/include...
https://github.com/torvalds/linux/blob/v6.1/arch/x86/include...
https://github.com/torvalds/linux/blob/v6.1/arch/x86/kernel/...
Linux パイプを「遅い」と呼ぶのは、Toyota Corolla を「遅い」と呼ぶようなもの。極端なユースケースでなければ十分速い
自動車レースをしているのか? しかも速さが技術より重要な種目なのか? それならもっと速い車を買えばいい。そうでなければ Corolla に乗り続ければいい
だから、ごく小さな効率向上であっても多くの時間をかけて最適化することには経済的に意味がある
10年以上パイプをさまざまな用途に使ってきたが、パイプ速度に詰まったことはなく、たいてい tar、gzip、find、grep、nc のようなツールがボトルネックだった可能性が高い。もちろん、これらのツールもやっていることを考えればかなり速い部類だ
FFMpeg は Non-Free SDK をソースに含められず、純粋な RGBA をファイルに保存するのはまったく非現実的だ。だからパイプが唯一の方法であり、高スループットなパイプが必要になる正当な理由がある
https://www.toyota.com/grcorolla/
この車はエンジニアリングと性能がすばらしく、本来意図していた GR Yaris を米国市場に持ち込みにくくした規則を回避するためのハックのような存在でもある。HN の人たちが軽く受け止めるだけのエンジニアリング/性能/ハック/市場の文脈はあると思う。しかも社長自身も今なお運転して扱っている
本文の核心とは別の話だが、現代の CPU では
rep movsbが最速のベクトル化版と同じくらい速い。CPU がこれを高速化していることが分かっているためだ。カーネル関数名の
copy_user_enhanced_fast_stringもそれを示唆している。関連する CPU 機能は ERMS(Enhanced Repeat Move String、一定以上の長さでrep movsbを高速化する)と FSRM(Fast Short Repeat Move String、短いコピーも高速化する)である。rep movsbはあるしきい値までは速いが、それを超えると通常のストア、または非一時ストア(non-temporal store)のほうが速い。すべてのしきい値は https://codebrowser.dev/glibc/glibc/sysdeps/x86_64/multiarch... に説明されている。
そしてこれらの値は固定でもなく、Noah Goldstein が今も毎年更新している。
memcpyについては、単純な C ループ版を削除してもよいほどrep movsbとrep stosbが速くなるのを、まだ待っている。memcpyをインライン化するように、可変長のmemcpy()もいつになったらインライン化してくれるのか気になる。AVX512 について記事で見かけなかった点がある。
xsave/xrstorのオーバーヘッド以外にも、AVX512 は電力を多く消費し、CPU 周波数スケーリングを引き起こす。詳しい内容と、それがどれほど微妙になり得るかは [1]、[2] を見るとよい。[1] https://www.intel.com/content/dam/www/central-libraries/us/e...
[2] https://www.intel.com/content/www/us/en/developer/articles/t...
ほぼあらゆる形の プロセス間通信 は「遅い」。安全性のために性能コストを払うことを選んでいるのだ。
特に、最速のプロセス間通信が長い関数呼び出しより遅くなければならない理由はあまりない。
また Hacker News の ハグ・オブ・デス を受けている。WordPress のページキャッシュのおかげで前回よりはましだが、それでもページ読み込みに数秒かかるかもしれないので了承してほしい。
そもそも
spliceがなぜそこまで遅くなければならないのか、よく理解できなかった。vmspliceより遅い理由としてバッファ割り当てとスカラー命令の使用を挙げていたが、なぜそれが必要なのか分からない。なぜ
spliceを単にvmspliceのように再実装できないのだろうか。きっと正当な理由があるはずだが、見落としているようだ。vmspliceはあらゆる種類のファイルディスクリプタで動作するわけではない。io_uring を使った版も見ると面白そうだ。カーネルとバッファをあらかじめ共有して一部のコピーを避けられそうだし、システムコールのオーバーヘッドも避けられそうだ。ただしここでは後者はほぼ無視できるように見える。
読み込みに 20 秒くらいかかるブログが言うには、かなり 大胆な主張 だ。
記事自体は素晴らしそうで、内部で何が起きているのか学べることが多い。