Promise::XS all() メソッド XS 再実装レポート
概要
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();
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
T2["then②\n(results,p1結果)"] T4["then④\n(results,p2結果)"] T6["then⑥\n(results,p3結果)"] 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
end
P1("p1") -->|"CALLBACK_ALL0"| state P2("p2") -->|"CALLBACK_ALL1"| state P3("p3") -->|"CALLBACK_ALL2"| state 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
DONE -->|true| ISREJ2{rejected?}
DONE -->|false| ISREJ{rejected?}
DECR --> ZERO{remaining == 0?}
ZERO -->|yes| BUILD["result = new RESOLVED\n全results[]をコピー"]
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)"
y-axis "倍率 (xs/perl)" 0 --> 32
発見したバグと修正
Bug 1 (Critical): output promiseの未捕捉rejection警告が出ない
code:mermaid
flowchart TD
end
A1 -->|"コピー作成"| A2 --> A3
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化できる余地がある (次の課題)
関連リンク