概要
- hyperpb はGo向けの高性能Protobufパーサライブラリ
- UPB の最適化をGoに移植しつつ、独自の最適化も多数導入
- Goの ランタイム特性 を活かした設計とJIT的PGO(プロファイルガイド最適化)対応
- 競合他製品と比較して圧倒的な性能 を実現
- 設計思想・API・内部構造・最適化手法の概要解説
Go向け高性能Protobufパーサ「hyperpb」の設計と最適化
- Protobuf の高速パーサ開発経験(C++/Rust/UPB統合)を活かしたGo用新規実装
- hyperpb はUPBのノウハウをGoに移植しつつ、Go独自の最適化も追求したライブラリ
- AMD Zen 4 環境でベンチマークを実施し、Go向け他パーサ(標準・vtprotobuf等)より高いスループットを記録
- hyperpbは ゼロコピー/アリーナ再利用/PGO など複数の最適化を段階的に適用可能
- 競合実装との比較ベンチマークでは、 全ての最適化を適用したhyperpbが圧倒的な性能 を発揮
生成型パーサの課題とUPBアプローチ
- 従来のProtobufパーサは 型ごとに専用コードを生成 する方式
- 型ごとに事前コンパイルが必要
- 多数の型を扱うと 命令キャッシュの効率悪化 や デコードスループット低下 が発生
- 巨大なswitch文やバイナリサーチによる分岐遅延
- UPB はCで書かれた動的パーサカーネル
- パーサはデータテーブルに基づく テーブル駆動型インタプリタ
- 各言語でC FFI経由で利用可能
- アリーナ最適化 による高速メモリアロケーション
hyperpbがGoでUPBを再発明した理由
- Goの cgo(C FFI) はパフォーマンス・メモリ管理・スケジューラ連携が非常に非効率
- CメモリとGo GCの相互運用が困難
- C呼び出しが遅く、Goスケジューラとの相性も悪い
- Go独自の特性を活かした新規実装を選択
- x86_64のレジスタABI でパーサ状態を全てレジスタ渡し
- Go特有の 未定義動作の少なさ を利用した「邪悪な」ポインタ最適化
- Goの 反射API (protoreflect)への最適化
- 起動時コスト許容 の文化を活かし、インタプリタのプログラムを ランタイム生成 (オンラインPGO/JIT)
hyperpbのAPI設計
- hyperpb.Compile 系関数でメッセージディスクリプタからhyperpb.MessageTypeを生成
- *hyperpb.MessageTypeから 新しいメッセージインスタンスを生成 し、proto.Unmarshalでデシリアライズ
- 反射APIを通じて Bufのprotovalidate 等のバリデーションライブラリと連携
- メッセージの ミューテーション(書き換え)は未対応 (主用途が読み取りのため)
- コンパイルは高コスト なのでキャッシュ推奨(regexp.Compileと同様の設計思想)
- PGOプロファイル を利用し、実際のワイヤデータに最適化した再コンパイルが可能
コア実装構造(internal/tdp配下)
- tdp :インタプリタ用「オブジェクトコード形式」の定義
- 型・フィールド情報の保持
- tdp/compiler :protoreflect.MessageDescriptorからtdp.Libraryへの変換ロジック
- tdp/dynamic :動的メッセージ型の定義とレイアウト管理
- tdp/vm :インタプリタVM本体、パーサ状態管理、varint解析・UTF-8検証の最適化ルーチン
- tdp/thunks :フィールドのアーキタイプ管理(約200種)
tdp.Typeとフィールド管理
- ルートメッセージから到達可能な全MessageDescriptorが tdp.Type として管理
- 各tdp.Typeは 動的サイズ・デフォルトパーサ・フィールド情報 を保持
- tdp.TypeParser はエンコード済みProtobufデータの解析用情報をコンパクトに保持
- 各フィールド/拡張ごとに tdp.FieldParser を持つ
- タグ対応・パース関数・次に試すフィールド情報 を持つことで分岐コスト最小化
- フィールドスケジューリング (宣言順・PGOによる「ホット度」考慮)で分岐予測最適化
- cold region 管理
- 大半のメッセージで未使用な拡張や低頻度フィールドは 別領域(cold region) に遅延確保
- 必要時のみcold regionを割り当て、メモリ効率と高速化を両立
- cold判定はPGOで動的に変化
パーサVMの最適化
- Goの ABIを最大限活用 し、パーサ状態(8つの64bit整数)をレジスタ渡し
- vm.P1/vm.P2 構造体で状態管理(Goコンパイラの制約回避のため分離)
- 全パーサ関数はこれら2構造体を引数・戻り値に持つことで レジスタ間のみで状態遷移
- スタック利用最小化 による高速化
hyperpbの今後の展望と参考情報
- さらなるPGO最適化 (分岐予測アルゴリズムの精緻化)に取り組み中
- internal/arena パッケージによるGCフレンドリーなアリーナ管理(詳細は別ブログ記事参照)
- 詳細・セールスポイントは Buf公式ブログ にも掲載
- 参考: Buf公式ブログ hyperpb紹介記事
- 元記事: Hacker News