- 最近議論されたI/O性能とCPU処理速度の不均衡を実験で検証し、実際には依然としてCPUが主要な制約であることを示す
- シーケンシャル読み取り速度はコールドキャッシュで1.6GB/s、ウォームキャッシュで12.8GB/sに達する一方、単一スレッドでの単語頻度計算は278MB/s程度にとどまる
- コードの**分岐(branch)**構造がベクトル化(vectorization)を妨げ、単純な小文字変換の最適化でも330MB/s程度までしか向上しない
wc -wコマンドですら245MB/sにすぎず、ディスクよりもCPU演算と分岐処理がボトルネックであることを確認
- AVX2ベースの手動ベクトル化で1.45GB/sまで引き上げたが、それでもシーケンシャル読み取り速度の約11%水準にとどまり、I/OではなくCPUがボトルネックであることを実証
I/O速度とCPU性能の比較
- Ben Hoytの主張に従い、最近のシーケンシャル読み取り速度の向上がCPU速度の停滞を追い越したのかを実験
- 同じ方法で測定すると、コールドキャッシュで1.6GB/s、ウォームキャッシュで12.8GB/sを記録
- しかし単一スレッドで単語頻度計算を行うと、278MB/sにすぎない
- これはキャッシュが温まった状態でもディスク読み取り速度の約1/5水準
Cベースの単語頻度計算実験
- GCC 12で
optimized.cを-O3 -march=nativeオプション付きでコンパイルし、425MBの入力ファイルで実行
- 結果: 1.525秒を要し、278MB/sの処理速度
- コード内の複数の分岐と早期終了がコンパイラのベクトル化最適化を妨げる
- 小文字変換ロジックをループの外に移した後、330MB/sに向上
- Clangを使うとベクトル化がよりうまく行われる
単純な単語数カウント(wc -w)との比較
- 頻度計算の代わりに、単純に単語数だけを数える
wc -wコマンドを実行
wcは' ', '\n', '\t'などのさまざまな空白文字やロケール文字を処理する
- 単純に空白だけを区切りとするコードより演算量が多い
AVX2ベースのベクトル化の試み
- 最新CPUの機能を活用し、AVX2命令セットでベクトル化を実装
- 256ビットレジスタを使用し、データを32ビット境界に整列
- 空白文字との比較のため
VPCMPEQB命令を使用
- ビットマスク(PMOVMSKB) と Find First Set(ffs) 命令で単語境界を検出
- Cosmopolitan libcの
strlen実装から着想
性能結果と結論
- 手動ベクトル化コード(
wc-avx2)は1.45GB/sの処理速度を達成
wc -wと同じ結果(82,113,300語)であることを検証
- コールドキャッシュ状態でも依然としてuserモードの演算時間が支配的
- ディスクI/OよりCPU演算がボトルネックであることを確認
- 全体としてディスク速度は十分に高速だが、分岐処理やハッシュ計算などのCPU演算が限界要因として残る
- コードと実験結果はGitHub(
haampie/wc-avx2)で公開されている
1件のコメント
Hacker Newsの意見
現代のCPUの性能限界は、単一コアが処理できるデータ量、つまり
memcpy()速度によって決まると考えているほとんどのx86コアは約6GB/s、Apple Mシリーズは約20GB/s程度だ
広告で言う「200GB/s」のような数値は全コア合算の帯域幅にすぎず、単一コアは依然として6GB/s付近にとどまる
したがって完璧なパーサーを書いてもこの限界は超えられない
しかしzero-copyフォーマットを使えば、CPUが不要なデータを飛ばせるので、理論上は6GB/sを「超える」ことができる
私が開発中のLite³フォーマットはこの原理を活用し、simdjsonより最大120倍高速な性能を示している
たとえばZen 1は単一コアで25GB/sを示す(参考リンク)
私が書いたmicrobenchmarkの結果では、Zen 2はAVX未使用時で17GB/s、non-temporal AVX使用時で35GB/sまで出る
Apple M3 Maxではnon-temporal NEONで125GB/sまで測定された
したがってx86が6GB/s、Appleが20GB/sという数値は実態よりかなり低い
iGPUが統合メモリにアクセスできるからだ
したがって大容量のメモリコピーや並列パース、圧縮/展開のような作業では、iGPUをblitterとして使うのが技術的に有利だ
ただしzero-copyフォーマットでいう「スキップ」はキャッシュライン単位で行われる
元記事の筆者は
timeコマンドの出力を誤って解釈しているようだsystem時間は、カーネルがプロセスの代わりに使ったCPU時間だ例で
real0.395s、user0.196s、sys0.117sなら、CPUは合計313msしか働いておらず、残りの82msはアイドル状態だったつまりディスクサブシステムより速く動いてはいたが、差はそれほど大きくない
またI/OパスはCPUバウンドの状態でもある — ディスクとコードが無限に速くても、カーネルのI/Oコード実行に117ms必要だ
記事の筆者です。続編があります: I/O is no longer the bottleneck, part 2
参加者が使ったさまざまな最適化手法を扱った分析記事が面白い
問題の複雑さや空白文字の分類数によってアプローチが変わっていた
性能ボトルネックは常に「CPUかI/Oか」のような単一要因ではなく、実際のワークロードで最初に飽和する資源だ
CPU、メモリ帯域幅、キャッシュ、ディスク、ネットワーク、ロック、レイテンシなど何でもありうる
だから計測し、プロファイリングで証明し、変更後に再び計測しなければならない
問題はCPUやI/Oではなく、**レイテンシ(latency)とスループット(throughput)**のバランスだ
ほとんどのソフトウェアはレイテンシを無視しているので遅い
データをメモリ上に線形配置したり、バッチ処理や並列化を適用すればずっと速くなる
CPU ↔ キャッシュ ↔ 不揮発性ストレージだけで構成されたアーキテクチャを想像してみる
mmap()がmalloc()と同じ性能特性を持つなら、プログラムメモリをファイル名で指定し、永続性をOSに任せることもできるはずだいまなお多くのソフトウェア設計がハードディスク時代の制約に縛られている
fsync()は依然として遅い真の永続性のためには、回転ディスクかどうかに関係なく別のアプローチが必要だ
実際、ほとんどのメモリ要求は
mmap()を通じて行われるただしカーネルがアクセスパターンを予測しにくいため、
read/writeより遅くなることがあるクラウド環境では性能が料金プラン調整の手段になることもある
ハードウェア性能は驚くほど進歩したが、一部のソフトウェア(特にWindowsやメッセンジャーアプリ)はむしろ遅く感じられる
開発者向けのリモートワークステーションとしては非効率だ
TelegramやFB Messengerは速いが、TeamsやSkypeはそうではない
一部のLCDには500msの遅延がある
NVMe SSDが初めて登場したとき、「これで2TB RAMを手に入れたようなものだ」と冗談を言っていた
ところが最近のGPUサーバーは実際に2TB RAMを搭載している — 驚くべきエンジニアリングだ
あのとき買っておけばよかったと悔やんでいる
高い同時実行性の環境でOLAPデータベースを最適化した経験では、ボトルネックの大半はメモリ速度だった
I/Oボトルネックは本来シーケンシャル読み取りではなく**シーク(seek)**時間に関する概念だった
記事の趣旨は理解できるが、この点は指摘しておきたい
シーケンシャル読み取り速度はコードで改善できないので、非シーケンシャルアクセスの最適化が重要だった