Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
Perl GraphQL 高速化バトル 2026年5月版
Search
AnaTofuZ
May 23, 2026
Technology
33
0
Share
Perl GraphQL 高速化バトル 2026年5月版
https://shonanka.connpass.com/event/389138/
のトークです
AnaTofuZ
May 23, 2026
More Decks by AnaTofuZ
See All by AnaTofuZ
k1LoW/deckのすすめ
anatofuz
0
670
Perl1.0 Deep Drive 0.01
anatofuz
0
200
Rubyの国のPerlMonger
anatofuz
3
1.6k
Pythonで爆速でHello, World!する
anatofuz
0
120
思いつきで推しの誕生日記念コンテンツを2日で作る技術
anatofuz
0
180
AWSで雰囲気でつくる! VRChatの写真変換ピタゴラスイッチ
anatofuz
0
500
令和最新版 Perlコーディングガイド
anatofuz
5
11k
rakulangで実装する! RubyVM
anatofuz
6
5.2k
沖縄の大学で育った学生がエンジニアになるまで
anatofuz
2
6.8k
Other Decks in Technology
See All in Technology
可視化から活用へ — Mesh化・Segmentation・アライメントの研究動向
gpuunite_official
0
240
キャリア25年目にしてTypeScript に出会うまで - 「型」を通じて振り返るプログラミング言語遍歴 / Meeting TypeScript After 25 Years in Tech - Looking Back at My Programming Language Journey Through "Types"
bitkey
PRO
1
120
TypeScriptはどのようにどこまで推論できるのか ─ とにかく as は禁止で
ypresto
0
250
AI時代に求められる思考のパラダイムシフト
nrinetcom
PRO
0
100
JaSSTに関わることで変わった人生観 #jasstnano
makky_tyuyan
0
160
R&D 祭 2024 UE5で絵コンテ・作画の制作支援ツールをつくる話
olmdrd
PRO
0
200
Terragrunt x Snowflake + dbt で作るマルチテナントなデータ基盤構築プラットフォーム
gak_t12
0
510
障害対応のRunbookは作った、でも本当に動くの? AWS FIS で EKS の AZ 障害を再現してみた
tk3fftk
0
120
20260515 ⾃分のアカウントとプライバシーを守る認証と認可の話〜利⽤者向け〜
oidfj
0
820
TypeScript の型で副作用の実行順序を制御する
yanaemon
0
120
実践 TanStack Start ― 新規プロダクトを開発して確立した、サーバーとクライアント境界の設計パターン / Practical TanStack Start Server-Client Boundary Patterns
kaminashi
1
150
【禁断】Obsidianの第二の脳に「知の巨人」と呼ばれた師匠の脳をロードしてみた
nagatsu
0
1.9k
Featured
See All Featured
世界の人気アプリ100個を分析して見えたペイウォール設計の心得
akihiro_kokubo
PRO
70
39k
Tips & Tricks on How to Get Your First Job In Tech
honzajavorek
1
510
How to Talk to Developers About Accessibility
jct
2
200
What's in a price? How to price your products and services
michaelherold
247
13k
Prompt Engineering for Job Search
mfonobong
0
310
The AI Search Optimization Roadmap by Aleyda Solis
aleyda
1
5.8k
Into the Great Unknown - MozCon
thekraken
41
2.5k
Marketing to machines
jonoalderson
1
5.3k
Code Review Best Practice
trishagee
74
20k
New Earth Scene 8
popppiees
3
2.2k
How GitHub (no longer) Works
holman
316
150k
Fireside Chat
paigeccino
42
3.9k
Transcript
Perl GraphQL 高速化バトル 2026 年5 月版 八雲アナグラ (@AnaTofuZ) 湘. なんか
#4 / 2026-05-23 graphql-perl → GraphQL::Houtou
自己紹介 八雲アナグラ @AnaTofuZ 📍 山梨県甲府市 💻 Web アプリケーションエンジニア Perl が好きですが最近はRuby
が主戦場です 🐪 Houtou.pm / 💎Kofu.rb たぶん7~8 月にOzara.pm ということでやるので はないか 🎮 最近の趣味は真・女神転生3 とデジモンです
GraphQL とは Meta が発表したクエリ言語 GraphQL API を提供するサーバー側と利用するクライアントに分かれる 宣言的UI とfragment colocation
の相性が最高 REST と GraphQL の違い REST GraphQL エンドポイント URL + HTTP メソッド 単一のところにPOST N+1 問題 複数エンドポイントを叩く DataLoader でPromise を作りバッチ化 型システム 使わなくても良い スキーマで厳密に定義
GraphQL のサーバーサイド処理 フェーズ 役割 ① パース クエリ文字列を解析し、どのフィールドを取得するか決定する ② 実行 フィールドごとにリゾルバを呼び出し、結果を
JSON に組み立てる ほぼ現代のプログラミング言語処理系と同じようなフローをたどる クエリ文字列 ──[ ① パース]──> 構文木(AST) ──[ ② 実行]──> レスポンス query { books(ids: ["1","2"]) { title author { name } } }
フェーズ①: パース — クエリ文字列 → AST ↓ パーサが構造を解析 実行エンジンはこの AST
を元に「どのリゾルバを・どの順で呼ぶか」を決定する パーサがクエリ文字列を走査して 構文木 (AST) に変換する query { books(ids: ["1","2"]) { title author { name } } } OperationDefinition (query) └─ Field: books ├─ Argument: ids = ["1", "2"] └─ SelectionSet ├─ Field: title └─ Field: author └─ SelectionSet └─ Field: name
フェーズ②: 実行エンジン ① books リゾルバ ids: ["1","2","3"] を受け取り、本のリストを返す ② title
/ author リゾルバ 各本オブジェクトから title 文字列と author オブ ジェクトを取得 ③ author.name リゾルバ 著者オブジェクトから name 文字列を取得 ④ JSON 組み立て 全フィールドの結果をネストした構造にまとめてレス ポンスを生成
GraphQL のAPI を作るには 基本的には普通のウェブサーバーとして実装する リファレンス実装はJavaScript で書かれている graphql-js 主要様々な言語でもライブラリが存在 Golang(gqlgen), Ruby(graphql-ruby),
… graphql-js とのコンパチ実装とその言語に最適化されたものがある GraphQL の仕様とは別に業界でよく使われる仕様もある Apollo Federation Relay 基本的に現代では困ることはない
None
Perl 老舗スクリプト言語 現代でも現役 高速化したいならXS(C 言語のPerl バインド用言語) を書く世界観 Rust でXS を書けるようにするみたいなアプローチもあるにはあるらしいが、現在でもXS/C
が現役 JIT とかRuby のRactor みたいな並列処理サポートはない 力強くマルチプロセスする世界観
GraphQL とPerl できなくはない graphql-perl CPAN 上にあるおそらく唯一まともに動くPerl のサーバーサイド実装 実際にいくつかのWeb サービスでも利用されている 詳しくは
👉 WEB+DB PRESS Vol.129
graphql-perl アーキテクチャ Pure Perl 実装の特徴 パーサ: Pegex XS を一切使わない Pure
Perl の PEG パーサ 実行エンジン: 再帰関数呼び出し フィールドの深さ × 幅だけ再帰スタックが積まれる 内部表現: ExecutionPartialResult フィールド数だけ HashRef の生成・破棄とマージが発生 → Perl の参照カウント管理のオーバーヘッド クエリ文字列を1 文字ずつスキャン ルールがマッチするたびに → Perl メソッド呼び出し → Perl の HashRef ノード生成 _execute_fields() → リゾルバ呼び出し → _complete_value() → _execute_fields() ← 再帰 → ... # すべてのフィールド結果が HashRef として受け渡される { data => $value, errors => [] } # フィールドの数だけ生成・破棄
実行エンジン: graphql-perl の場合 (1/2) 最終目標: {data => ..., errors =>
[]} の HashRef を組み立てて JSON 化する books リゾルバ呼び出し オブジェクト型 → 再帰して次ページへ フィールド列挙 (Query 階層) → [book1, book2, ...] フィールド列挙 (Book 階層) ↓
実行エンジン: graphql- perl の場合 (2/2) Book 階層では title ・ author
の2 フィールドを列 挙してリゾルバを呼ぶ author もオブジェクト型なので再帰 Author 階層で name リゾルバを呼び、スカラー値を確 定 HashRef マージ 確定したスカラー値を下から上へ積み上げ、最終的な {data, errors} 構造を組み立てる コストが積み重なる箇所: 再帰のたびに HashRef を生成・マージ フィールドの深さ × 幅だけすべて Pure Perl で繰り 返す
title リゾルバ author リゾルバ オブジェクト型 → 再帰 name リゾルバ フィールド列挙
(Book 階層) → 'Perl 入門' スカラー型 → 確定 → {id:10, name:'Alice'} フィールド列挙 (Author 階層) → 'Alice' スカラー型 → 確定 HashRef マージ {data=>{books=> [{title,author:{name}}]}, errors=>[]} graphql-perl のつらいところ 相性がいいのかというと言語特性的な意味で結構大変 並列処理のAPI が言語レベルで無いのでPromise 作っても直列実行 ストリーミング処理がprefork のモデルだとつらい いわゆるモダンなPerl のモジュールをかなり利用している Role::Tiny, Tipe::Tiny, Moo… Web アプリケーションを書く上では使うのには問題ないが、フレームワークという立場だとパフォー マンスがボトルネック PurePerl のウェイトが強いので遅い Pegex でパースをしているのでなかなか遅い 構文解析式文法(PEG )と正規表現(Regex )のライブラリ DSL を喰わせると正規表現で動的に処理系が生成される Pegex 自体は機能のリッチさと比較してもとても高速 なんだがGraphQL 特化ではないので…
graphql-perl のつらいところ ほぼ開発が止まっており最近のGraphQL 仕様に追従できてない Apollo Federation とかも当然非対応 そもそもPerl 界隈でGraphQL やってる人が少ない
今から新規開発でGraphQL をPerl でやりたいかというと…? Perl 人工も減っているため… というわけで遅いので気合いのモンキーパッチを様々な人が作ったり、運用でカバーしているのが実情
遅いのでどうにかしたい GraphQL サーバーのライブラリをつくる≒GraphQL の言語処理系を実装する PurePerl で遅いのがわかっているのでXS を使わざるを得ない Ruby の用にJIT が効いたりするわけでもない
XS で実装するとなった場合C 言語でGraphQL の処理系を全部書くことになる 主に時間と実装量でつらい…
とはいえ2026 年 生成AI が台頭してきた 驚くべきことに現代のAI エージェントはXS/C をかなり生成できる メモリリーク関連はコンテキストにperldoc のXS ベストプラクティスを喰わせるとかなり減る
Perl のGraphQL ライブラリについては豊富なテストがある そもそもGraphQL なので正しい挙動は言語問わず転がっている Perl という面でもgraphql-perl がすでにあるのでAPI の互換性を保てばテストとしては同じ という訳でXS で書いていくぞ GraphQL ライブラリ自体 周囲で使っているPromise ライブラリ
Promise ライブラリとGraphQL Perl はPromise っぽい概念が言語コアにない 今Future っていう名前でリファレンス実装がすすみつつはある 早いPromise を使うならPromise::XS 特にGraphQL
だとDataLoader で使う # DataLoader が各 author を非同期で並行取得する仕組み my $deferred = Promise::XS::deferred(); # 未解決の Promise を作る $deferred->resolve($author_data); # 後で値を入れる Promise::XS::all(@promises) # 全 Promise が揃ったら次へ ->then(sub { dispatch_batch(@_) });
Promise::XS 基本的にXS で書かれたPromise ライブラリ AI ブームの以前からちょくちょくAnaTofuZ がコミットしてた 一部のメソッドが実装難易度からPerl で書かれていた 特に
all がPerl で書かれているけどDataLoader でめちゃくちゃ使う とりあえずここをXS 化した なんかメンテ権ももらったのでcpan-release した
GraphQL::Houtou ということで次はXS/C でのGraphQL サーバーの実装プロジェクトを開始 GraphQL::Houtou graphql-perl 完全コンパチではないのでGraphQL::XS ではない Houtou 宝刀
ほうとう( 山梨) 引退した推しのVTuber の宝灯桃汁 基本的には生成AI に書かせる
雑に作ってるのでPR がでかい https://github.com/AnaTofuZ/GraphQL-Houtou/pull/4 https://github.com/AnaTofuZ/GraphQL-Houtou/pull/5
最初のプロンプト graphql-perl のコードを喰わせて「XS 化していきましょう」 「AST をそのまま実行すると遅いと思うのでIR 化してください」 API はそのままで
GraphQL::Houtou compiled_ir アーキテクチャ XS パーサ + 型別 C 専用処理 graphql-perl
からの改善点: Pegex を廃止 → XS パーサ(約80 倍速) スキーマ・クエリを事前コンパイル → 実行計画を キャッシュ フィールドループを C で処理 型別 C 専用処理: 問題: C 専用処理の外に出ると Perl に戻る ~60k ops/s で頭打ち Object/List/Abstract フィールド → C の専用処理で完結 → 高速 その他(汎用ケース) → Perl の汎用処理へ C 専用処理 ↓ 対応外のケースに到達 ↓ Perl HashRef を生成 (C→Perl) ↓ Perl の汎用処理を呼ぶ ↓ また C に戻る (Perl→C) ↑ フィールド数 × リクエスト数 繰り返す
実行エンジン: compiled_ir の場合 (1/2) C 専用処理(高速路) Object/List 型フィールドは C の中だけで処理が完結す
る 汎用落ち パターンに合わない場合は Perl の汎用処理に落ちる → C →Perl →C の境界コストが毎フィールドで発生 この分岐がフィールドの数だけ繰り返される = フィールドが多いほど境界越えのコストが積み重な る
Object/List C 専用処理 汎用ケース XS 呼び出し execute_xs_raw() C フィールドループ フィールドの型は?
C 専用処理で完結 native outcome → 次ペー ジ Perl HashRef を生成 C→Perl 境界コスト → 次ペ ージ 実行エンジン: compiled_ir の場合 (2/2) C 専用処理の場合 native outcome のまま Writer に直接蓄積 → 境界越え なし 汎用落ちの場合 Perl HashRef を作って Perl 側で処理 → 次の XS 関数に渡すときまた HashRef から値を取り 出す → 「C →Perl →C 」の往復コストが1 フィールドごとに発 生 結果: C 専用処理を増やすほど境界越えも増え、改善が 頭打ちに
Perl→C 境界コスト C 専用処理 native outcome Perl HashRef {data=>, errors=>[]}
Writer に蓄積 Perl 汎用処理 HashRef から値を再取り出 し レスポンス XS 化 ワイ「XS にすればとりあえず早くなるやろ」
XS 化 ワイ「XS にすればとりあえず早くなるやろ」 👉そんなことはない!!!
中途半端にXS にすると遅い 1. 関数呼び出しスタックの設定(C の呼び出し規約に合わせる) 2. 引数の型変換: Perl の SV*
→ C の型 3. 参照カウントの更新(Perl のメモリ管理) 4. 戻り値の型変換: C の型 → Perl の SV* これがGraphQL のオブジェクトの分実行されるぞ!!! 遅いな!! Perl の世界とXS の世界は行き来するとコストがかかる
初回アーキテクチャの問題 API はそのままをその通りに受け取り、ユーザーが触れないAPI もそのままになった 結果的に部分的にXS/Perl を相互に行き来することになりクソ遅い XS で書いてるのにPerl のオブジェクトを無限にアロケーションしてるので実質Perl で書いてる
XS に行くだけでコストが増える段階 そもそもAST をユーザーが触ること無いでしょ 自分で書いてたらサンクコストがあるがAI 太郎なので捨ててリアーキテクチャ
リアーキテクチャ 遅いので「ユーザーが使う関数呼び出しのAPI だけ同じだったら何もしてもよい」という感じで実装し直し AI 太郎がC 書くのがめんどくさいということだったのでPurePerl で一旦複雑な箇所を実装し直す 当初の予定通り以下の感じ 基本的に実行はXS で書いたVM
を使う IR を多段コンパイルして最適化する AST のデバッグは情報が増えてつらいのでオプトインでよい WebApp ではスタートアップ時にいろいろできるのでキャッシュを効きやすくしてほしい
GraphQL::Houtou 第4 世代 (NativeBundle) 実行全体を C に閉じる アーキテクチャ: Perl に戻るのは入り口と出口の2
回だけ XS opaque handle: 遅延 materialize: GraphQL::Houtou::execute() (Perl) ↓ XS 呼び出し(境界越え 1 回) ↓ vm_runtime.h (C ループ) ↓ block/op を C で直接回す ↓ フィールド解決 × N ↓ Writer (C 構造体)に蓄積 ↑ Perl にレスポンス返却(境界越え 1 回) # Perl からはオブジェクトに見えるが # 内部は C 構造体へのポインタ bless( \$ptr, 'GraphQL::Houtou::Runtime::Cursor' ) // args は C のまま保持 // リゾルバ呼び出し直前にのみ Perl HashRef を生成 if (payload->static_args_sv) { return SvREFCNT_inc(...) // 2 回目以降はキャッシュ }
実行エンジン: GraphQL::Houtou 第4 世代 の場合 C ループがフィールドを処理 Perl に戻るのはリゾルバ呼び出し時だけ compiled_ir
との違い: C →Perl への折り返しがない(汎用落ちがない) 中間 HashRef の生成がない ループ自体が C なので再帰スタックが不要 XS 呼び出し (Perl→C 境界 1 回) C ループ vm_runtime.h op を順番に処理 op: books C でリゾルバ呼び出し準備 Perl リゾルバ callback → [book1, book2, ...] op: title / author C でリゾルバ呼び出し準備 Perl リゾルバ callback → 'Perl 入門' / {id:10} Writer (C 構造体) 結果を蓄積 最後に1 回 JSON 組み立て Perl → C (1 回) C ループ ├─ books: C で準備 → Perl callback → C に戻る ├─ title: C で準備 → Perl callback → C に戻る └─ name: C で準備 → Perl callback → C に戻る Writer に蓄積 (C のまま) 最後に JSON 化(1 回) C → Perl (1 回)
例題: WEB+DB PRESS Vol.129 電子書籍 API — 記事のサンプルコード N+1 問題の解決策:
DataLoader + Promise::XS Promise::XS::all() でDataLoader がバッチで作ったPromise を集約 type Query { book(id: ID!): Book! } type Book { id: ID! title: String! author: Author! } type Author { id: ID! name: String! } # DataLoader.pm ( 記事のサンプルより) sub load { my ($self, $key) = @_; my $deferred = Promise::XS::deferred(); $self->{batch_map}{$key} = $deferred; return $deferred->promise; } # App::GraphQL より { resolve => \&Promise::XS::resolved, all => sub { Promise::XS::all(map { ... } @_) } }
計測結果: 全4 パターンの比較 構成 計測時間 実行文数 関数呼び出し数 graphql-perl + PXS
0.20 327ms 1,124,694 379,675 graphql-perl + PXS 0.21 327ms 1,110,459 362,826 Houtou compiled_ir ( リアーキ前) 144ms 291,629 119,598 Houtou 第4 世代 ( 現在) 138ms 414,486 126,285 クエリ: books(ids: ["1","2","3"]) { id title author { id name } } — 3 冊 + 各Author
Flamegraph: graphql-perl + PXS 0.20 Flame Graph Reset Zoom Search
Ro.. GraphQL::Execution::_complete_value Mo.. Moo:.. G.. G.. Gr.. GraphQL::.. GraphQL::Directive.. GraphQL.. Graph.. GraphQL.. GraphQL::Sc.. Mo.. m.. Moo::ex.. Role::.. GraphQL::Execution::_complete_value.. GraphQL::Execution::_exec.. GraphQL::Execution::_complete_va.. G.. Graph.. T.. GraphQL::Execution:.. Moo:.. Context::dispatch_all Gr.. GraphQL::Execution::_complete_value GraphQL:.. Role.. DataLoader::dispatch Mo.. GraphQL::Schema::BEGIN@11 Ty.. GraphQL::Execution::_complete_.. Role:.. G.. Promise::XS::Deferred::.. Moo::with GraphQL::Execut.. Ro.. m.. Role.. Gr.. Moo::_.. Moo::_.. Moo:.. main::BEGIN@6 T.. Moo.. G.. GraphQL::Execution::_complete_v.. Moo::R.. Moo::.. Gra.. Gr.. Graph.. Role::T.. Moo::_U.. T.. Moo::_U.. GraphQL::Sche.. Mo.. GraphQL::Type::List::_complete_value Gr.. Gr.. Gr.. GraphQL::Type::Object::_compl.. GraphQL::Execution::_execute_operation Gra.. Moo::_s.. Gr.. GraphQL::Type::Ob.. GraphQL::Execution:.. Mo.. Moo:.. Mo.. GraphQL::Execution::_complete_value.. Rol.. G.. main::run_once GraphQL::Execution::_complete_va.. GraphQL::Execution::execute G.. GraphQL::Execution.. Graph.. GraphQ.. T.. GraphQL::Execution::_execute_fields Graph.. T.. 観察ポイント ホットパスに Pure Perl が並ぶ: GraphQL::Execution の再帰呼び出し Pegex パーサの文字スキャン Promise::XS::all → Promise::XS::Promise- >all (Perl) _complete_value / _execute_fields の相互再 帰
Flamegraph: Houtou compiled_ir Flame Graph Reset Zoom Search Moo::_Utils::_require GraphQL::Houtou::Directive::BEGI..
main::BEGIN@11 G.. Types.. Type::L.. M.. GraphQL::Houtou::Execution::execute Rol.. Gra.. main::run_once M.. Ro.. GraphQL::Houtou::Directive::B.. Role::Tiny::apply_roles_t.. Grap.. G.. Type.. GraphQL::Houtou::XS::Execution::.. GraphQL::Houtou::Schema::BEGIN@11 G.. Type.. M.. GraphQL::Houtou::Sche.. T.. Grap.. T.. GraphQL::Houtou::XS::Execution::.. M.. T.. Mo.. E.. Mo.. Moo::with Gra.. Mo.. Role::Tiny::_check_roles G.. GraphQL::Error.. Ty.. Ty.. GraphQL::Houtou::Role:.. Moo::_Utils::_load_module Type::.. Ty.. Type:.. G.. Moo::Role::_require_module Ty.. Typ.. Mo.. GraphQL::Ho.. M.. Moo.. M.. Type:.. Typ.. G.. compiled_ir 世代(リアーキ前) XS パーサ + 型別 C 専用処理: パーサは XS (Pegex を廃止) フィールドループは C の専用処理で実行 ただし対応外のケースで Perl HashRef に折り返す NYTProf では: execute_xs_raw が 203 回呼ばれ 合計 8.5ms 残りは Perl 側の型チェックやスキーマ構築 # GraphQL::Houtou::Execution より return GraphQL::Houtou::XS::Execution::execute_xs_raw( $schema, $doc, ... ); # → XS が compile_ir を呼ぶ
Flamegraph: Houtou 第4 世代 Flame Graph Reset Zoom Search M..
GraphQL::Houtou::Runtime::OperationCompiler::compile_ope.. Grap.. Gr.. GraphQL::Houtou::execute E.. main::run_once Role::Tiny::With::with Grap.. Role::Tiny::_require_module Gra.. GraphQL::Hout.. Gra.. Graph.. Role::Tiny::_check_roles Grap.. Role::Tiny::apply_roles_to_package Gra.. Ty.. Type.. Ty.. GraphQL::Error::BEG.. Role::Tiny::_load_module GraphQL::Houtou::Runtime::SchemaGraph::_compile_native_pr.. GraphQL::Error::BEGIN@8 GraphQL::Houtou::Runtime::SchemaGraph::compile_program G.. E.. GraphQL::Ho.. main::BEGIN@10 GraphQL::H.. Mo.. G.. GraphQL::Houtou::Role::Leaf::BEGIN@9 G.. G.. GraphQL::Houtou::Runtime::Oper.. GraphQL::Houtou::Schema::BEGIN@10 G.. Gra.. Type:.. Type:.. G.. T.. Gr.. GraphQL::Houtou::Runtime::NativeRuntime::execute_document GraphQL::Houtou::Runtime::OperationCompiler::_.. G.. Type.. GraphQL::Houtou::Runtime::NativeRuntime::compile_program GraphQL::Houtou::Directive::BEGIN@11 Graph.. G.. T.. Type.. Ty.. Type::.. Graph.. Gr.. NativeBundle 完全C 化 フレームグラフが著しく薄い! NativeBundle アーキテクチャ: Perl → XS 呼び出し (1 回) ↓ C ループ (vm_runtime.h) ↓ フィールド解決 × N ↓ Writer (C 構造体) ← Perl にレスポンス返却 (1 回)
第4 世代の工夫① XS opaque handle フィールド100 個 → HashRef 100
回生成 GraphQL::Houtou の内部表現 実行ループ中は Perl オブジェクトを作らない → 最後の1 回だけ {data => ..., errors => []} を 生成 graphql-perl の内部表現 # フィールドの解決結果 = Perl HashRef my $result = { data => { title => "Perl 入門" }, errors => [], }; # 毎フィールドごとに生成・破棄 # Perl から見ると「オブジェクト」 # 実体は C 構造体へのポインタ bless( \$ptr, 'GraphQL::Houtou::Runtime::Cursor' ) # ↑ ここに C struct のアドレスが入っている // C 側では構造体として直接操作 gql_runtime_vm_cursor_t *ptr = INT2PTR(...); ptr->block_index = ...;
第4 世代の工夫② 遅延 materialize (args) 第4 世代: C 内に保持、必要な時だけ生成 args
なし → singleton 返却(0 アロケーション) 静的引数( limit: 10 )→ 初回のみ生成、以降キ ャッシュ リスト1000 件でも HashRef 生成は 1 回だけ 従来: 毎回 Perl HashRef を作る フィールド解決 → args HashRef 生成 → リゾルバ呼出 フィールド解決 → args HashRef 生成 → リゾルバ呼出 フィールド解決 → args HashRef 生成 → リゾルバ呼出 (リスト1000 件なら1000 回) // NativeBundle の中では引数を C のまま保持 gql_runtime_vm_native_args_payload_t { char **names; IV count; SV *static_args_sv; // キャッシュ先(最初はNULL ) }
第4 世代の工夫② 遅延 materialize (response) 状況 従来 遅延 materialize args
なしフィールド 毎回空の HashRef 生成 singleton (0 アロケーション) 静的 args ( limit: 10 ) リストN 件でN 回生成 初回のみ、以降キャッシュ レスポンス組み立て ループ中に都度 HV/AV 生成 Writer が最後に1 回まとめて生成 実行ループ中はフィールド解決結果をPerl オブジェクトではなくC の構造体化 C 実行ループ └─ フィールド解決 → Outcome (C 構造体) └─ フィールド解決 → Outcome (C 構造体) └─ ... └─ 最後に1 回だけ → {data => HV, errors => AV} を生成
第4 世代の工夫③ 3 層キャ ッシュ 型情報・リゾルバ関数ポインタ・ABI コードを C 構造体としてプロセス起動後に一度だけ構築 第2
層: クエリコンパイルキャッシュ(初回のみ) NativeProgram は JSON シリアライズ可能 → Persistent Query へ 第3 層: op 内キャッシュ NativeProgram が再利用されるほどヒット率が上がる 毎リクエストでやること(これだけ) 1. NativeProgram をキャッシュから取得 2. 変数の準備( prepare_variables() ) 3. C ループで実行 4. Writer でレスポンス生成 パース・コンパイル・C 構造体構築はすべてスキップ 第1 層: スキーマキャッシュ(起動時1 回) sub build_native_runtime { # 2 回目以降はキャッシュを返す return $self->{_compiled_native_runtime} if $self->{_compiled_native_runtime}; ... } クエリ文字列 → NativeProgram (C 構造体) (変数値を含まない実行計画) → 同じクエリなら何度でも使い回せる // static_args_sv が埋まっていれば即返す if (payload->static_args_sv) { return SvREFCNT_inc(payload->static_args_sv); } // 初回だけ HashRef を作る
第2 層の応用: Persistent Query デプロイ時・起動時(一度だけ) リクエスト時(パース・コンパイルなし) NativeProgram は JSON にシリアライズしてディスクに保存できる
# クエリをコンパイルして JSON 保存 $schema->dump_program_descriptor($document, '/cache/hero_query.json'); # NativeRuntime + NativeProgram をまとめて保存 $schema->dump_native_bundle_descriptor($document, '/cache/hero_bundle.json'); # JSON → NativeProgram (C 構造体)を復元 my $program = $schema->load_program_descriptor('/cache/hero_query.json'); # そのまま実行(変数だけ渡す) $schema->build_native_runtime->execute_program( $program, variables => { id => '123' }, );
クエリ文字列のパース・コンパイル・C 構造体構築をすべてスキップ リクエストパスでは「変数の準備 → C ループ実行 → レスポンス生成」だけ 第4 世代のベンチマーク
世代 ops/sec 対 graphql-perl graphql-perl 数百/s 基準 旧GraphQL::Houtou (compiled_ir ) ~56,878/s 約100x 第3 世代 Milestone B (NativeBundle 初期) ~343,901/s 約600x 第4 世代(各種最適化後) ~650,345/s 約1000x 超 同期実行( nested_variable_object クエリ)
世代ごとの遍歴 世代 頭打ちの原因 突破した方法 graphql-perl 全処理が Pure Perl 、参照カウントの オーバーヘッド
XS パーサ・C 専用処理の導入(旧 GraphQL::Houtou ) 旧GraphQL::Houtou C 専用処理の端で毎回 Perl HashRef に折り返す アーキテクチャを全面刷新(第3 世代) 第3 世代 Perl VM Perl ループで XS 関数を毎 op 呼ぶ境 界コスト vm_runtime.h の C ループに置き換え(第4 世代) 第3 世代 NativeBundle 初期 Perl wrapper が多く、呼び出しごと に境界越え Perl wrapper を thin facade/XSUB-only に 縮退 第4 世代(現在進行中) async パスに Perl 境界が残る Promise の継続スケジューリングを C ルー プ内に閉じる
現在の実装 graphql-perl の機能はだいたい網羅した Apollo Federation あたりと @oneOf みたいなのを実装中
感想 AnaTofuZ はXS/C はある程度読めるのでなんかやらかしてそうなのはコードみてわかるのはよい コンパイラの知見もそこそこ生きている XS/C がアセンブラみたいな勢いでコード生成されているのでぶっちゃけ全部見るのはかなり難しい ベンチマークスクリプトをいれてスコアリングするとISUCON 感がつよい 愚直にAPI
移植させようとするとわりとカスみたいな試行錯誤ルーチンになる 「ある程度まとめてからベンチマークして」みたいに言ってあげる必要がある なにかしらのXS を作っていきましょう