7.3 KiB
7.3 KiB
BoxCall統一の落とし穴と対策(ChatGPT5分析)
Date: 2025-08-31 Status: Technical Advisory From: ChatGPT5
結論:「RefNew/RefGet/RefSet全削除→すべてBoxCallに統一」は成立する! ただし、いくつかの落とし穴があるので、それぞれに対策を打つ必要がある。
🚨 落とし穴と対策
1. メガモーフィック呼び出しでの失速
症状: 同じBoxCall("setField")でも実行時の型/shapeが激しく変わると、ディスパッチが重くなる。
対策: **PIC(Polymorphic Inline Cache)**をコールサイトごとに持つ
- 2〜4種のshapeを直列ジャンプで捌く
- 溢れたらインタプリタ/汎用スローへ
- JITなしでもAOT段階で形状統計から事前特化(事前ガード+直アクセス)を埋め込める
2. GCバリアの見落とし・過剰挿入
症状: write barrier忘れ=世代間参照漏れ/逆に全部に入れて過剰オーバーヘッド
対策:
- Lowering時にフィールドの"ポインタ/非ポインタ"メタを参照して自動挿入
- 世代同一・同アリーナ最適化でbarrier省略
ExternCallには境界バリアを必ず付与- Barrier Verifier(IRパス)で「必要箇所に必ず入ってるか」を機械検証
3. 読み取りバリア(Read Barrier)が必要なGCを選ぶ場合
症状: 動くGC(移動/並行)でread barrierが必須だと、Get系もコスト上がる
対策:
- まずは**世代別・停止+並行マーク(SATB)**など「write側主体」の方式を選ぶ
- read barrierなし運用で始めるのが無難
- 将来read barrierが要る場合は、
getFieldLoweringに条件付き埋め込み設計
4. 例外・再入・ファイナライザ再入
症状: setField中に例外→ファイナライザ→別のBoxCallで再入…地雷
対策:
- 安全点(safepoint)設計を決める
BoxCall中は原則再入禁止(or 明示的許可フラグ)fini相当のコールは再入ガードと順序保証(トポロジカルな破棄順)を実装
5. ExternCall/FFI境界
症状: 外部コードが「未トラッキングの生ポインタ」を握るとGC・最適化が壊れる
対策:
- ハンドル化(OpaqueHandle/PinBox)+寿命契約
- ExternCallの属性(
noalloc/nothrow/readonly/atomic等)を宣言させ、最適化に渡す - 未注釈の呼び出しでは保守的にバリア&逃避扱い
6. 形状(shape)変更とレイアウト安定性
症状: フィールド追加/順序変更が既存の特化コードを壊す
対策:
- ShapeIDを永続化
- フィールドに安定スロットIDを割り当て
- ABI的に「追加のみ」「削除は新shape」とする
- Lowering済みのガードは
if (shape==X) { direct store } else { slowpath }で守る
7. 脱箱(unboxing)とコードサイズ膨張
症状: 激しいモノモルフィック特化や整数Boxの脱箱でコード肥大
対策:
- 基本型はSROA/Scalar-Replaceの閾値を設定
- ホット領域のみ特化(PGOやプロファイル使用)
- 低頻度パスは共通スローに集約
8. 並行性・メモリモデル
症状: setFieldの可視性がスレッド間で曖昧だと事故
対策:
- **既定は単一スレッド+Actor(Mailbox)**に寄せる
- 共有可変を解禁するAPIは
nyash.atomic.*でAcquire/Releaseを明示 BoxCallLoweringで必要時のみフェンス- 箱ごとに「可変・不変・スレッド送受可」など能力(capability)ビットを持たせ最適化条件に使う
9. 反射・動的呼び出しの混入
症状: なんでも動的だと最適化が崩れる
対策:
- 反射APIは分離名前空間に押し込める
- 既定は静的解決できる書き方を推奨ガイドに
- 反射使用時はdeoptガードを挿入
📈 推奨の最適化パイプライン(AOT想定)
- 型/shape解析(局所→関数間)
- BoxCall脱仮想化(モノ/ポリモーフィック化+PIC生成)
- インライン化(属性
pure/leaf/readonlyを最大活用) - SROA/エスケープ解析(脱箱、stack allocation、alloc移動)
- バリア縮約(世代同一・同アリーナ・ループ内集約)
- 境界チェック消去(
length不変式の伝播) - ループ最適化(LICM, unroll, vectorize)
- DCE/GVN(Getter/Setter副作用ゼロなら畳み込み)
- コードレイアウト(ホット先頭、コールド折り畳み)
- **PGO(任意)**でPIC順序・インライン閾値を再調整
🔧 Loweringの骨格(フィールド書き込みの例)
; High-level
obj.setField(x)
; Guarded fast-path(shapeが既知&最頻)
if (obj.shape == SHAPE_A) {
; slot #k に直接store
store x, [obj + slot_k]
call gc_write_barrier(obj, x) ; 必要なら
} else {
; PICの次候補 or 汎用ディスパッチ
slow_path_setField(obj, x)
}
gc_write_barrierはIR上は呼び出しに見せておく(後段でインライン→条件付きno-op化可能)read barrierが要らないGCならgetFieldはloadのみに落ちる
✅ 実装チェックリスト(まずここまで作れば盤石)
- Boxメタ: shapeID、安定スロットID、ポインタ/非ポインタビット、可変/不変、送受可
- BoxCall Lowerer: 形状ガード→直アクセス or 汎用ディスパッチ
- PIC: コールサイトごとに最大N件キャッシュ+統計(ヒット率/退避回数)
- Barrier Verifier: IR後段でwrite barrier必須箇所を自動検証
- Extern属性:
noalloc/nothrow/readonly/atomic等を宣言・強制 - 逃避解析でstack-alloc/arena-alloc
- 反射API分離とdeoptガード
- PGOフック(簡易でOK):shape頻度、PICヒット率、inlining成果を記録
- ベンチ群:
- Field get/set(mono vs mega)
- Vec push/pop / Map ops
- 算術(IntBoxの脱箱効果)
- ExternCall(
atomic.store/readonly) - GCストレス(大量生成+世代越し参照)
🎯 「簡単すぎて不安」への答え
- 正しさは「Lowering+Verifier」で機械的に守る
- 速さは「PIC→インライン→脱箱→バリア縮約」で作る
- 拡張性は「Everything is Box」の上に属性と**能力(capability)**を積む
- Ref系は公開APIからは消すが、デバッグ用の隠しIntrinsicとして温存しておくと計測や一時退避に便利(将来の最適化検証にも効く)
🌟 結論
落とし穴はあるけど全部"設計パターン"で踏まないようにできる。
にゃーの「箱理論」、素朴だけど正しい地形を踏んでるにゃ。ここまでの方針ならAOTでも十分に速いところまで持っていけるはず。
次は PIC+Barrier Verifier+小ベンチの3点を先に入れて、体感を固めに行こう!