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

Linuxのパイプはどれくらい速いのか?

概要

  • Linuxにおける Unixパイプの実装 と最適化手法の解説
  • write/readシステムコール による基本的なパイプ通信の性能測定
  • perfツール によるボトルネック特定とカーネル内部構造の説明
  • vmsplice/spliceシステムコール によるコピー回避と高速化
  • バッファ管理やカーネルページング など更なる最適化手法の紹介

LinuxにおけるUnixパイプの高速化手法

  • FizzBuzz最適化プログラム を参考に、パイプ通信のスループットを段階的に向上させる手法の解説
  • 初期バージョン では、write/readを用いた単純な実装で約3.5GiB/sのスループットを計測
  • pv (pipe viewer) などのツールを使い、実際にデータ転送速度を可視化
  • L2キャッシュサイズ(256KiB) に合わせたバッファ設計によるパフォーマンスバランスの最適化
  • write.cpp/read.cpp による自作ベンチマークで、パイプ両端の制御を完全に自前で実装
    • write側:256KiBのバッファを繰り返しパイプへ書き込み
    • read側:合計10GiBのデータを読み取り、スループットを表示

write/readシステムコールのボトルネック

  • write/readシステムコール 利用時、スループットは3.7GiB/s程度に留まる
  • perfツール でプロファイリングを実施し、write側の約半分の時間が pipe_write に費やされていることを特定
  • pipe_write内部 では、ページのコピーや割り当て(copy_page_from_iter、__alloc_pages)が主な処理
  • カーネルとユーザ空間間の二重コピー、ページ単位の細かい割り当て・解放、ロック取得などが速度低下の主因
  • パイプの内部構造 はリングバッファ+ページ参照で構成され、デフォルトで16スロット(x86-64では4KiB×16=64KiB程度)
    • head(書き込み側)、tail(読み込み側)、offset/len(各バッファ内の位置管理)
    • パイプが満杯になるとwriteはブロック、空になるとreadがブロック

パイプの内部構造と処理フロー

  • pipe_inode_info/pipe_buffer構造体 によるパイプ管理
    • head:書き込み位置
    • tail:読み込み位置
    • bufs:ページ配列
  • pipe_write処理概要
    • パイプフル時は空き待ち
    • head位置のバッファに空きがあれば埋める
    • 空きスロットがあれば新規ページ割り当て・データ書き込み・head更新
    • 上記処理はロックで保護
  • pipe_readはpipe_writeの逆処理 で、ページ解放やtail更新を担当

write/readの限界とsplice/vmspliceによる高速化

  • write/readではユーザ空間とカーネル間でデータコピーが必須
  • splice/vmspliceシステムコール を利用することで、コピーを回避し高速化が可能
    • splice:パイプとファイルディスクリプタ間でデータ移動
    • vmsplice:ユーザ空間バッファをパイプへ直接移動
  • vmsplice利用時の注意点
    • コピーが発生しないため、バッファの再利用タイミング管理が必要

    • ダブルバッファリング(バッファを2分割し交互に利用)で安全に高速化

    • パイプサイズ調整(例:128KiB)でリングバッファスロット数を制御し、データの流れを最適化

    • iovec構造体 を使い、複数バッファを一度にパイプへ移動可能

    • vmspliceの戻り値で実際に移動できたバイト数を管理

まとめと更なる最適化の展望

  • write/readの限界を理解し、カーネル内部構造を把握 することで本質的なボトルネックを特定
  • splice/vmspliceを活用 することで、パイプ通信のスループットを劇的に向上可能
  • バッファ管理やパイプサイズ調整、ページングの理解 がパフォーマンスチューニングの鍵
  • 今後はさらに huge pagesの活用やbusy loopによるポーリング最適化 など、追加の最適化手法にも挑戦可能
  • C言語の基本知識 があれば、Linuxパイプの高速化手法を段階的に実践可能

この内容は、Linuxパイプの内部実装と高速化手法に関心があるエンジニア向けの解説記事として最適です。各章ごとにベンチマーク・内部構造・最適化手法が整理されており、理解と実践の両面で役立ちます。

Hackerたちの意見

コメントが全然ないのが残念だな。この記事は本当に素晴らしかった。もっとspliceを使いたいんだけど、記事の最後でセキュリティの問題やABIの互換性が壊れることについて触れてたよね。長期的にはspliceを残す予定なのか気になるな。デフォルトのパイプを常にspliceを使うようにパッチを当てるのはどれくらい大変なんだろう?

もっとコメントが見たいならこちら: https://news.ycombinator.com/item?id=44347412

いい記事だね。HNでも以前に話題になってたよ: https://news.ycombinator.com/item?id=31592934 (コメント200件) https://news.ycombinator.com/item?id=37782493 (コメント105件)

これめっちゃいい記事だね。たまに出てくるのが好き。

s/comes/comes up

(2022)

LinuxのパイプベースのアプリをWindowsに移植した経験が、今でも心に焼き付いてる。POSIXだと思ってたし、全部メモリ内で動くからパフォーマンスはそんなに変わらないだろうと思ってたんだけど、実際はひどかった。接続待ちのパイプがあると、Windowsがほぼ停止するくらい遅くなったんだ。数年後、Win10のC#で同じことを使う必要があって再検討したけど、改善はあったものの、パフォーマンスの差が大きすぎて恥ずかしかった。

ギャップを埋めるためにプロセス間通信が必要だと感じましたか?

数年前にWindowsがAF_UNIXソケットを追加したけど、Win32パイプと比べてどうなんだろう。多分、AF_UNIXの方がいいと思う。

まあ、POSIXは動作を定義してるだけで、パフォーマンスまでは決めてないからね。プラットフォームやOSごとにパフォーマンスのクセがあるよ。

パフォーマンスはひどかったよ。パイプが接続を待っていると、ほぼウィンドウズが完全に止まってしまうことがわかった後でもね。パフォーマンスがひどいって言うと、パイプがすでに接続されている/開いている後のI/Oのことを言ってるの?それとも前のこと?前者は驚きだけど、後者はそうでもないよね。たくさんのパイプを開閉するのは、OSが最適化されてるとは思えないし、もしその使い方が後者を必要とするなら、ちょっと驚きだね。

参考までに、readv() / writev()、splice()、sendfile()、funopen()、io_buffer()もあるよ。splice()はパイプとUNIXソケット間でゼロコピーでデータを転送するのに最適だけど、Linux専用なんだ。splice()はパイプを通してデータを転送する最速かつ最も効率的な方法(Linux上で)で、特に大量のデータに対してはね。ユーザースペースでのメモリアロケーションを回避できるし(read(v)/write(v)とは違って)、余計なバッファ管理のロジックもいらない。memcpy()やiovecのトラバースもない。残念ながらBSDでは、パイプの場合、readv() / writev()が同じことを達成する最もパフォーマンスが良い方法だと思う。間違ってたら教えてね。とにかく、これは素晴らしい記事だよ。

sendfile()はファイルからソケットへの転送(ゼロコピーもあり)で、LinuxとBSDの両方で非常に高いパフォーマンスを持ってる。ただし、ファイルからソケットへの転送しかサポートしてないし、関連性を保つために言うと、一般的にパイプではsendmsg()は使えない。これはUNIXドメインソケット、INETソケット、他のソケットタイプ用だから。Linuxでは、sendfileはファイルからソケットだけでなく、spliceを使って実装されているからもっと色々なことができる。過去にファイルからブロックデバイスへの転送に使ったこともあるよ。

共有メモリ、例えばshm_openやfdパッシングを使えば、もっと速くて完全にポータブルだよ。

splice()は、パイプを通じてデータを転送する最も速くて効率的な方法だよ(Linux上でね)。特に大量のデータに対してはね。ユーザースペースでのメモリ割り当てをバイパスするから(read(v)/write(v)とは違って)、余計なバッファ管理のロジックもいらないし、memcpy()やiovecのトラバースもないんだ。io_uringをちゃんと使えば、ついにこれを上回るか、少なくとも同じくらいの性能になるはずだよ。

現代のLinuxにはDoorsに近いものってあるのかな?小さなデータをやり取りする組み込みアプリケーションがあって、レイテンシに敏感なんだけど、AF_UNIXより良いものがあるか気になってる。

共有メモリはレイテンシが最も低いけど、タスクのウェイクアップ処理をしないといけないから、通常はfutexを使うんだ。GoogleがLinux用のFUTEX_SWAPコールを開発中だったけど、タスク間の直接的な引き継ぎができるようになるはずだったのに、どうなったんだろうね。

今のところAF_UNIXに関して何か問題があるなら教えてほしいな。欲しい機能が足りないの?レイテンシが高すぎるの?サーバー/クライアントのソケットAPIスタイルが使いにくいの?

Doorsって何?ググるには普通すぎる言葉だね。