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

I/Oはもはやボトルネックではないのか? (2022)

2026年1月6日原文(stoppels.ch)

概要

  • Ben Hoyt のブログ記事を受けて、I/Oがボトルネックではないとする主張の検証
  • CPU最適化 やベクトル化によるパフォーマンス向上の試み
  • wc -w などの既存ツールや手動ベクトル化との速度比較
  • AVX2 による手動最適化の実践と結果
  • ディスク速度とCPU速度の関係 についての考察

I/Oは本当にボトルネックか?ベンチマークと最適化の実践

  • Ben Hoyt のブログで「I/Oはボトルネックではない」と主張
  • 順次読み込み速度 は近年大幅に向上、CPU速度は頭打ち傾向
  • cold cache で1.6GB/s、 warm cache で12.8GB/sの読込速度を計測
  • 単一スレッド で1.6GB/sのワード頻度カウントは可能かという疑問
  • GitHub にコードを公開

Cによる最適化実装の検証

  • Ben Hoyt の高速C実装を GCC 12 で-O3 -march=native付きでコンパイル
  • 425MB のテキスト(聖書100冊分)を入力として実行
  • 結果は 278MB/s (warm cache)と期待外れの速度
  • ホットループに分岐や早期脱出が多く、 ベクトル化困難 であることが判明

ベクトル化による改善

  • 小文字変換部分をループ外に出すことで 330MB/s に改善(clang使用)
  • しかし、依然としてcold cacheの順次読込速度の5分の1程度
  • ハッシュマップのキャッシュミスやパーフェクトハッシュ導入などの余地もあるが、劇的な改善は難しいと判断

問題を単純化して計測

  • 頻度カウントをやめて単純なワード数カウント( wc -w)を実行
  • 結果は 245.2MB/s とさらに低速
  • wc -w は多様なホワイトスペースやロケール対応で処理が重い点を指摘

AVX2による手動ベクトル化とビットトリック

  • AVX2 などの新しいCPU命令セットでのベクトル化を試行
  • コンパイラの自動ベクトル化は困難、分岐の多いスカラプログラムの限界
  • VPCMPEQB でホワイトスペース位置をマスク化し、 PMOVMSKB でビットマスクをintへ
  • ffs (Find First Set)命令でワード開始位置を効率的に特定

実装と検証

  • immintrin.h を使った手動AVX2実装
  • データを32バイトアラインし、ループを4回アンローリングして 128バイト ずつ処理
  • 実装上のバグ修正に苦戦しつつも動作確認
  • wc-avx2wc -w で同一結果を確認

パフォーマンス結果

  • warm cache1.45GB/s (順次読込速度の11%程度)
  • cold cache でもユーザーモード処理の割合が高い
  • ディスク速度の向上に対し、CPU側の処理が追いついていない現状

まとめと展望

  • ディスクI/O速度の向上 は著しいが、 CPU側の処理最適化 がボトルネック
  • 自動ベクトル化 の限界、手動最適化の必要性
  • GitHub でコード公開、さらなるビットトリックや最適化案の募集
  • 今後も CPUアーキテクチャコンパイラ技術 の進化に期待

Hackerたちの意見

I/Oがボトルネックになるのは、シーケンシャルリードのことじゃなかったよね?記事のポイントは分かるけど。

現代のCXL/PCIeを考えると、RAMやメモリコントローラーが徐々にI/Oになってきてるって言うのも馬鹿げてはいないと思うけど。

初めてデータベースの授業を受けたとき、トピックの一つが古いハードドライブのシークタイムで測定されたI/Oパフォーマンスだった。I/Oができる速度よりも早くシーケンシャルリードのためにコードを最適化することはできないから、実際にシーケンシャルでないものを最適化することに集中するのが一番なんだ。

現代のCPUのパフォーマンス限界は、シングルコアに通すことができるデータ量なんだよね。要するにmemcpy()の速度。ほとんどのx86コアではその限界は約6 GB/s、AppleのMチップだと約20 GB/sだよ。『200 GB/s』みたいな広告の数字は、全体のメモリ帯域幅、つまり全コアを合わせたものだからね。個々のコアではやっぱり6 GB/s前後が限界なんだ。だから、完璧なパーサーを書いても、それ以上は速くならない。JSONやProtobufみたいなデータの(デ)シリアライズにもこの限界が適用されるんだ。これらのフォーマットは、フィールドを読み取る前に完全にパースされる必要があるからね。でも、ゼロコピー形式を使えば、CPUは関係ないデータをスキップできるから、6 GB/sの限界を「超える」ことができるんだ。俺が作ってるLite³シリアライズ形式は、まさにこれを利用していて、いくつかのベンチマークではsimdjsonを120倍も上回る性能を出せるんだよ。https://github.com/fastserial/lite3

6 GB/s サムスンが14 GB/sのシーケンシャルリード速度を謳ったNVMe SSDを売ってるね。

ここでのアーキテクチャの限界って何なんだろう?個々のコアとキャッシュ、またはメモリコントローラー間のバス?

かっこいいね、lite3にスキーマモードを追加してメッセージサイズのトレードオフをなくすことって可能だと思う?ほとんどの人は、シリアライズとデシリアライズの両方でハードスキーマを使いたがると思うけど、スキーマなしでも動くのはいいよね。

あなたのシングルコアの数値は、ピークスループットとしてはかなり低いように思えます。全コアがアクティブで帯域幅を争っていると仮定しない限り、例えばデュアルチャンネルのZen 1がシングルコアで25GB/sを示しているのに対してです。https://stackoverflow.com/a/44948720 私はシングルスレッドのmemcpyのためのマイクロベンチマークをいくつか書きました。Zen 2(8チャンネルDDR4)のナイーブCでは17GB/s、非一時的AVXでは35GB/s。Xeon-D 1541(2チャンネルDDR4、私の最も弱いシステムで10年前のもの)では、ナイーブCで9GB/s、非一時的AVXで13.5GB/s。Apple Siliconのテスト(ウォーム=新しいソースバッファを生成し、出力バッファをmemset(0)して、メモリフェンスを追加し、同じコピーを再実行)では、M3のナイーブCで冷却時17GB/s、ウォーム時41GB/s、非一時的NEONで冷却時78GB/s、ウォーム時78GB/s。M3 MaxのナイーブCでは冷却時25GB/s、ウォーム時65GB/s、非一時的NEONでは冷却時49GB/s、ウォーム時125GB/s。M4 ProのナイーブCでは冷却時13.8GB/s、ウォーム時65GB/s、非一時的NEONでは冷却時49GB/s、ウォーム時125GB/s。実際、なぜApple Siliconのウォームが冷却よりもずっと速いのかはよく分からないです。ソースバッファは各イテレーションごとに新しいランダムデータで埋められていて、メモリフェンスも使っているのに、キャッシュよりもずっと大きい16GBのソース/デスティネーションバッファでスピードアップが見られます。x86/Linuxでは冷却/ウォームのテストの違いはありませんでした。私の推測では、カーネルのページアカウンティングに関する何かで、CPUとは関係ないと思います。だから、x86で6GB/sのシングルコア制限や、Apple Siliconで20GB/sの制限を主張するのは理解できません。

Liteはインプレースで修正できると主張してるけど、文字列のような可変長構造体でそれがどう機能するのか気になるな。

ほとんどのx86コアでは制限は約6GB/sで、Apple Mチップでは約20GB/sです。Mシリーズがx86の3倍の帯域幅を持つ理由は何ですか?

最近のいくつかのチップ(Apple Mシリーズも含めて)では、iGPU(統合メモリにアクセスできる)を使わないとメモリ帯域を飽和させることができないんだ。CPUコアだけでは無理だよ。だから、iGPUを使って大きなメモリ内転送やスループットが制限される計算(並列パースや圧縮/解凍のワークロードなど)を行うのが技術的に推奨される選択肢になったんだ。> ただし、ゼロコピー形式を使うと、CPUは気にしないデータをスキップできるから、6 GB/sの制限を「超える」ことができるんだ。もちろん、「スキップ」はキャッシュラインによるものだよ。キャッシュラインは、メモリスループットの観点から見て自己完結したデータのブロックだから、どんな部分を読んでも残りはタダでついてくるんだ。

ただし、ゼロコピー形式を使うと、CPUは気にしないデータをスキップできるから、6GB/sの制限を「超える」ことができるよ。ただ、64バイトのキャッシュラインを一度にロードしなきゃいけないし、ほとんどのCPUはある程度のリードアヘッドを行うから、これらの利点を得るにはかなり大きな「空白」スペースが必要になるよ。典型的なprotobufよりも大きいね。

Hacker Newsで議論の続きを見る