- Sep 0.10.0は、AMD 9950X(Zen 5)のようなAVX-512対応CPU向け最適化により、低レベルCSVパースで21 GB/sを達成し、従来の約18 GB/sより高速化
- 性能向上は、.NET 9.0のAVX-512コード生成で発生するマスクレジスタの往復というボトルネックを減らすよう、パーサー構造を変更したことによるもの
- 新しいAVX-512-to-256パーサーは、512ビットで
charをロードした後に256ビットのバイトベクターへ変換し、マスク処理と追加の並べ替えコストを回避
- Sepの低レベルパース性能は、2023年の0.1.0における5950X/.NET 7.0での約7 GB/sから、0.10.0の9950X/.NET 9.0での約21 GB/sへと約3倍改善
- 上位ベンチマークでも、9950XのマルチスレッドSepはpackage assets 100万行を72.213ms、約8.0 GB/sで処理し、floatsデータでも約8.1 GB/sを記録
Sep 0.10.0の目標と結果
- Sep 0.10.0は2025年4月22日にリリースされ、AMD 9950X(Zen 5)のようなAVX-512対応CPU向け最適化と9950Xベンチマークを含む
- 低レベルCSVパースにおいて、Sepは9950Xで21 GB/sを達成
- 0.10.0以前は、同じ9950Xで約18 GB/sだった
- 分析対象はpackage assets CSVデータの低レベル
Rowsパースであり、すべての数値はシングルスレッド基準
- ベンチマーク値は数パーセントポイント変動しうるため、特定のリリースで小さな回帰が見られる場合がある
0.1.0から0.10.0までの性能変化
- Sepの性能は、コード変更、.NETバージョンの変化、CPU世代の変化が重なりながら段階的に向上してきた
- 代表的な性能推移は以下の通り
- 0.1.0, 5950X, .NET 7.0: 約7 GB/s
- 0.3.0, 5950X, .NET 8.0: 約12 GB/s
- 0.6.0, 5950X, .NET 9.0: 約13 GB/s
- 0.9.0, 9950X, .NET 9.0: 約18 GB/s
- 0.10.0, 9950X, .NET 9.0: 約21 GB/s
- 2023年6月のSep公開以降、2年弱で約3倍高速化
- 5950XでSep 0.9.0を使った場合と、9950XでSep 0.10.0を使った場合を比較すると、約1.6倍改善
- 9950Xのブーストクロックは5.7GHz、5950Xは4.9GHz
- このクロック差だけでも約1.2倍は説明できると見ている
.NETのAVX-512コード生成におけるマスクレジスタのボトルネック
- Sepは0.2.3からAVX-512をサポートしていたが、当時の.NET 8はAVX-512の
k1-k8マスクレジスタを明示的にはサポートしていなかった
- 従来のAVX-512コード生成では、比較結果がマスクレジスタに入った後、一般レジスタへ移され、さらに再びマスクレジスタへ戻される流れが発生していた
- 9950Xへアップグレード後、Sep 0.9.0は低レベルCSVパースで約18 GB/sを記録し、5950Xより約1.4倍高速だった
- 環境変数でパーサーを切り替えて比較したところ、9950XではAVX2パーサーが約20 GB/sを記録し、従来のAVX-512パーサーより約10%高速だった
- この差から、AVX-512のマスクレジスタ処理が依然として性能に影響していることが確認された
Sepパースループの基本構造
- Sepのすべてのパーサーは同じ基本構造に従っており、
Parseジェネリックメソッド1つで、クォート処理の有無が異なる2つの経路をサポートする
ParseColInfos: クォート処理ありで使用し、より多くの状態追跡が必要
ParseColEnds: クォート処理なしで使用
- パースは配列から取得した
char span単位で行われ、例では16Kサイズ
- このサイズはCPUキャッシュに収まるほど小さく、その後の効率的なマルチスレッド化にも有利
- ループでは、SIMDレジスタに16ビット文字データをロードし、それをバイトSIMDレジスタへ変換した後、CSV特殊文字と比較する
- 比較対象には
\n, \r, ", ;などが含まれる
- 比較結果はビットマスクへ変換され、マスク内でセットされたビットのみが順次パースされる
- 性能差は、このSIMD C#コードが.NETでどのようなマシンコードへJITコンパイルされるかに大きく左右される
既存のAVX-512パーサーと0.10.0での調整
- 0.9.0の
SepParserAvx512PackCmpOrMoveMaskTzcntは、2つの512ビットSIMDレジスタにそれぞれ32個のcharをロードし、それらを1つの512ビットバイトベクターへパックして、ループあたり64文字を処理する
- パック後のデータは順序が入れ替わっているため、
PermuteVar8x64で再整列する必要がある
- .NET 9.0のアセンブリでは、各
Vec.Equalsがvpcmpeqbとvpmovm2bの2命令に展開され、k1のようなマスクレジスタとzmm汎用ベクターレジスタとの間の移動が繰り返されていた
- Sep 0.10.0では、
MoveMask呼び出しをより前段に移し、マスクレジスタと一般レジスタの間の往復回数を削減
- 他のパーサーでは、「特殊文字がない」高速経路で命令数を減らすため、必要なときだけ
MoveMaskを呼び出す
- 調整後もマスクレジスタから一般レジスタへの移動は残るが、全体のアセンブリ命令数は減少した
AVX2と新しいAVX-512-to-256パーサー
- AVX2ベースの
SepParserAvx2PackCmpOrMoveMaskTzcntのアセンブリは、マスクレジスタがないため、より直線的な構造になっている
- この構造により、AVX2パーサーは従来の0.9.0 AVX-512パーサーより高速だった
- 0.10.0の新しい
SepParserAvx512To256CmpOrMoveMaskTzcntは、AVX-512命令でcharをロードした後、ConvertToVector256ByteWithSaturationで256ビットのバイトベクターを作成する
- 実際の命令は
vpmovuswb
- ループあたりの処理量は「わずか」32個の
charだが、構造はより単純
- この方式は512ビットのマスクレジスタ問題を回避し、パック済みデータもすでに正しい順序で
ymm4に入っているため、追加の並べ替えも不要
- この新パーサーにより、9950XでのSepパース性能は約21 GB/sまで引き上げられた
パーサー別の9950X低レベルベンチマーク
- AMD 9950Xで環境変数を使ってすべてのパーサーを実行・比較した結果、新しいAVX-512-to-256パーサーが最速だった
- 主な結果は以下の通り
SepParserAvx512To256CmpOrMoveMaskTzcnt: 21597.7 MB/s, 27.0 ns/row, 1.351ms
SepParserVector256NrwCmpExtMsbTzcnt: 20608.5 MB/s, 28.3 ns/row, 1.416ms
SepParserAvx2PackCmpOrMoveMaskTzcnt: 20599.3 MB/s, 28.3 ns/row, 1.417ms
SepParserAvx512PackCmpOrMoveMaskTzcnt: 19944.3 MB/s, 29.3 ns/row, 1.463ms
Vector256ベースのクロスプラットフォームパーサーは、AVX2とほぼ同水準に達した
Vector128およびVector512ベースのクロスプラットフォームパーサーも依然高速だが、5〜10%遅く、Vector512はVector128より遅かった
SepParserIndexOfAnyは2787.0 MB/sと大きく後れを取り、Vector64は9950Xで高速化されず459.9 MB/sにとどまった
5950Xと9950Xの上位ベンチマーク
- package assetsデータでの100万行処理結果では、9950Xは5950Xより大幅に高速だった
- 5950X
Sep_MT: 119.430ms, 4888.1 MB/s
- 9950X
Sep_MT: 72.213ms, 8084.1 MB/s
- 9950Xでのシングルスレッド
Sepは、package assets 100万行を291.979ms、1999.4 MB/sで処理
- 同じ9950Xのpackage assetsベンチマークでは、比較対象は以下の性能を示した
Sylvan: 413.265ms, 1412.6 MB/s
ReadLine_: 377.033ms, 1548.4 MB/s, 割り当て量 1991.04MB
CsvHelper: 1005.323ms, 580.7 MB/s
- floatsデータでも、9950XのマルチスレッドSepは25,000行を2.497ms、8136.8 MB/sで処理した
- 5950Xから9950Xへの上位ベンチマークの改善幅は、低レベルベンチマークと同様に約1.5〜1.6倍
まだコメントはありません。