Promise::XS all() メソッド XS 再実装レポート
#Perl #XS #Promise #パフォーマンス改善
概要
PerlのPromiseライブラリ Promise::XS の all() メソッドをPure Perl実装からXS(C拡張)に再実装した記録
バージョン: Promise::XS 0.20 / Perl 5.42.0 / macOS (Apple Silicon)
変更コミット: 101f3fb (実装) → 7cbdd75 (バグ修正)
背景
then() / catch() / finally() はすでにXSで実装済みだった
しかし all() だけはPerlで実装されており、ボトルネックになっていた
READMEにも「all() and race() should ideally be implemented in XS」と記載あり
変更前のPerl実装
code:perl
# lib/Promise/XS/Promise.pm
sub all {
my $all_done = Promise::XS::resolved();
for my $p (@_1 .. $#_) {
my @results;
$all_done = $all_done->then( sub {
@results = @_;
return $p;
} )->then(sub{ ( @results, @_ ) } );
}
return $all_done;
}
問題: N個のpromiseに対し 2N 個の中間promiseと 2N 個のPerlクロージャを生成
アーキテクチャ比較
Perl実装: 直列チェーン構造
code:mermaid
flowchart LR
R"resolved()"
T1"then①\n@results=@_\nreturn p1"
T2["then②\n(results,p1結果)"]
T3"then③\n@results=@_\nreturn p2"
T4["then④\n(results,p2結果)"]
T5"then⑤\n@results=@_\nreturn p3"
T6["then⑥\n(results,p3結果)"]
OUT"output promise"
R --> T1 --> T2 --> T3 --> T4 --> T5 --> T6 --> OUT
P1("p1") -.resolved.-> T2
P2("p2") -.resolved.-> T4
P3("p3") -.resolved.-> T6
N=3の場合: 1+2*3=7個のpromiseオブジェクト + 6個のクロージャ
XS実装: 並列コールバック構造
code:mermaid
flowchart LR
subgraph state"pxs_all_state_t (共有状態)"
CNT"remaining: 3→2→1→0"
RES["results0..2"]
DONE"done: false→true"
end
P1("p1") -->|"CALLBACK_ALL0"| state
P2("p2") -->|"CALLBACK_ALL1"| state
P3("p3") -->|"CALLBACK_ALL2"| state
state -->|"remaining==0"| OUT"output promise\n(1個のみ)"
N=3の場合: 1個のoutput promise + 3個のコールバック構造体 + 1個のstate構造体
実装の詳細
追加したC構造体
code:c
/* 共有状態 */
struct pxs_all_state_s {
xspr_promise_t* output; // 出力promise
unsigned total; // 入力数
unsigned remaining; // 未解決数
bool done; // 完了/reject済みフラグ
SV** results; // 結果格納配列 (AV refの配列)
int refs; // 参照カウント
};
/* 例外安全のためのガード (SAVEDESTRUCTOR_X用) */
typedef struct {
xspr_promise_t* output;
pxs_all_state_t* state;
} pxs_all_guard_t;
コールバック処理フロー (XSPR_CALLBACK_ALL)
code:mermaid
flowchart TD
START(callback fires) --> DONE{state->done?}
DONE -->|true| ISREJ2{rejected?}
ISREJ2 -->|yes| SUPPRESS"rejection_should_warn = false\n(二重警告抑制)"
ISREJ2 -->|no| SKIPskip
DONE -->|false| ISREJ{rejected?}
ISREJ -->|yes| CLONE"pxs_result_clone()\n↓\norigin: warn=false\nclone: warn=true"
CLONE --> FINISH_REJ"xspr_promise_finish\n(output, clone)"
FINISH_REJ --> SET_DONE"state->done = true"
ISREJ -->|no| STORE["resultsindex = values..."]
STORE --> DECR"remaining--"
DECR --> ZERO{remaining == 0?}
ZERO -->|yes| BUILD["result = new RESOLVED\n全results[]をコピー"]
BUILD --> FINISH_RES"xspr_promise_finish(output, result)"
FINISH_RES --> SET_DONE2"state->done = true"
ZERO -->|no| WAIT他のpromiseを待つ
XSファイルへの主な変更箇所
xspr_callback_type_t 列挙型に XSPR_CALLBACK_ALL を追加
xspr_callback_s 共用体に all { state*, index } メンバを追加
xspr_callback_process() にALL処理ブランチを追加
xspr_callback_free() にALL解放処理を追加
新関数を追加
pxs_all_state_decref() … 参照カウントが0になったらstate/results/outputを解放
xspr_callback_new_all() … XSPR_CALLBACK_ALLコールバックを生成
_pxs_all_guard_cleanup() … SAVEDESTRUCTOR_X用クリーンアップ関数
all(...) XSUBを追加 (MODULE = Promise::XS::Promise セクション)
lib/Promise/XS/Promise.pm の sub all { ... } を削除
パフォーマンス比較
速度: pending promise (Deferred作成→all()→resolve の往復)
table:speed_pending
N perl_all xs_all 倍率
1 551,973/s 1,280,169/s 2.3x
10 62,250/s 217,105/s 3.5x
100 3,997/s 23,345/s 5.8x
速度: resolved promise (すでに解決済みのpromiseを渡すスループット)
table:speed_resolved
N perl_all xs_all 倍率
1 776,211/s 3,468,509/s 4.5x
10 84,663/s 1,116,991/s 13.2x
100 4,885/s 146,345/s 30x
メモリ: 理論値 (C構造体サイズ計算, 64bit)
table:memory_theory
N perl_all xs_all 削減率
1 384 bytes 216 bytes 1.8x
10 2,688 bytes 648 bytes 4.1x
50 12,928 bytes 2,568 bytes 5.0x
100 25,728 bytes 4,968 bytes 5.2x
メモリ: 実測値 (RSS差分, batch=3000)
table:memory_rss
N perl_all/call xs_all/call 削減率
1 1,163 bytes 16.4 bytes 71x
10 12,381 bytes 76.5 bytes 162x
100 124,655 bytes 923.0 bytes 135x
Perlクロージャ (@results を捕捉する2N個のCODE ref) のメモリコストが特に大きい
Nと速度倍率の関係
code:mermaid
xychart-beta
title "XS / Perl 速度倍率 (resolved promise)"
x-axis "N=1", "N=10", "N=100"
y-axis "倍率 (xs/perl)" 0 --> 32
bar 4.5, 13.2, 30
発見したバグと修正
Bug 1 (Critical): output promiseの未捕捉rejection警告が出ない
code:mermaid
flowchart TD
subgraph before"修正前 (バグあり)"
S1"origin->result\nrejection_should_warn = false" -->|"同じ構造体ポインタ"| S2"output->result\nrejection_should_warn = false ← バグ"
S2 --> S3"catchしなくても\n警告が出ない ❌"
end
subgraph after"修正後"
A1"origin->result\nrejection_should_warn = false"
A2"pxs_result_clone()"
A3"clone->result\nrejection_should_warn = true"
A1 -->|"コピー作成"| A2 --> A3
A3 --> A4"catchしなければ\n警告が出る ✅"
end
原因: xspr_result_t 構造体をinputとoutputで共有していたため、rejection_should_warn=false がoutputにも伝播した
修正: pxs_result_clone() でoutput用に独立したコピーを作成
code:c
// 修正前
origin->finished.result->rejection_should_warn = false;
xspr_promise_finish(aTHX_ state->output, origin->finished.result); // 同じ構造体
// 修正後
xspr_result_t* rejection = pxs_result_clone(aTHX_ origin->finished.result);
origin->finished.result->rejection_should_warn = false; // sourceを抑制
xspr_promise_finish(aTHX_ state->output, rejection); // outputは独立
xspr_result_decref(aTHX_ rejection);
Bug 2 (Medium): 例外発生時のメモリリーク
code:mermaid
sequenceDiagram
participant XS as all() XSUB
participant Perl as Perl runtime
Note over XS: 修正前
XS->>XS: Newxz(output) ✅
XS->>XS: Newxz(state) ✅
XS->>Perl: xspr_promise_from_sv() → croak!
Perl-->>XS: longjmp (CLEANUPなし)
Note over XS: output と state がリーク ❌
Note over XS: 修正後
XS->>XS: Newxz(guard)
XS->>Perl: SAVEDESTRUCTOR_X(guard)
XS->>XS: Newxz(output) → guard->output = output
XS->>XS: Newxz(state) → guard->state = state
XS->>Perl: xspr_promise_from_sv() → croak!
Perl-->>XS: longjmp
Perl->>XS: _pxs_all_guard_cleanup() が自動呼び出し ✅
Note over XS: output と state が解放される ✅
修正: SAVEDESTRUCTOR_X で pxs_all_guard_t を登録
正常終了時はポインタをNULLにしてデストラクタをno-opにする
バグ修正のパフォーマンスへの影響
table:bug_fix_perf_impact
修正内容 影響箇所 コスト
pxs_result_clone() 追加 (Bug 1) rejectionパスのみ Newxz 1回 + SV コピー N個。resolveパスはゼロ影響
SAVEDESTRUCTOR_X 追加 (Bug 2) all()初期化時のみ Newxz 1回 + セーブスタック登録。全体の1%未満
修正前後でパフォーマンスの実質的な差はなく、Perl実装比での優位性は維持された
既知の残課題
xspr_promise_then 内の Renew クロークによるリーク
xspr_promise_then() 内部の Renew() (コールバック配列の拡張) がOOM時にcroak
その時点でcallbackは割り当て済みだが promise->callbacks には未登録 → リーク
これは all() 固有の問題ではなく then() / catch() / finally() でも同様
XS全体の問題として別途対処が必要
デバッグの困難さ
XSのバグはセグメンテーション違反として現れ、Perlスタックトレースが得られない
本番環境での問題調査には gdb + コアダンプ解析が必要
ビルド・デプロイの複雑化
コード変更のたびに make による再コンパイルが必要
Perl バージョンアップ時にバイナリの再ビルドが必要
CPUアーキテクチャ (x86_64/ARM64) ごとに個別ビルドが必要
まとめ
Perl実装を完全にXSに置き換えることで 速度2〜30倍・メモリ70〜160倍の改善 を達成
バグ修正によるパフォーマンスへの影響はほぼゼロ
Critical バグ (警告抑制) は修正済み / Medium バグ (例外安全性) は主要パスを修正済み
race() も同様にXS化できる余地がある (次の課題)
関連リンク
FGasper/p5-Promise-XS (GitHub)
perlxs - XS言語リファレンス
perlguts - Perlの内部構造