Perceus

GC なしの精密参照カウント (RC) + reuse 解析アルゴリズム。Reinking, Xie, de Moura, Leijen (Microsoft Research) による PLDI 2021 論文 (Distinguished Paper Award)。Koka 言語のメモリ管理の中核。

論文の核心的主張: たった3つの RC 操作ルールだけで、GC なしのメモリ管理が完結する。 さらにこの3ルールの上に reuse analysis と drop specialization を重ねることで、関数型プログラミングが命令型と同等のメモリ効率を達成する。

RC 操作の3ルール

線形リソース計算 (λ₁) 上で、すべての RC 操作を3つのルールに帰着させる。

Rule操作意味生成されるコード
Rule 1dup(x)変数をコピーするとき RC を増加rc_inc(x)
Rule 2drop(x)変数が不要になったとき RC を減少rc_dec(x) → RC==0 なら free(x)
Rule 3recursive dropRC==0 で解放するとき、子フィールドを再帰的に dropdrop(x.f1); drop(x.f2); free(x)

この3ルールの前提: λ₁ では 各変数はちょうど1回使用される。複数回使うなら明示的に dup、使わないなら明示的に drop。コンパイラが自動挿入するのでプログラマは意識しない。

Rule 1: dup — コピー時に RC 増加

変数が複数回使用される場合、コンパイラが dup を挿入する。

fun map(xs, f)
  match xs
    Cons(x, xx) ->
      dup(f)              // f は f(x) と map(xx, f) の2箇所で使用 → dup
      val y  = f(x)
      val ys = map(xx, f)
      Cons(y, ys)
    Nil -> ...

原則: dup は可能な限り遅く。 使用直前まで引き延ばし、不要な dup を回避する。実際に両方の分岐を通るとは限らないため、導出の葉まで押し出す。

Rule 2: drop — 不要時に RC 減少

変数がその後使用されない場合、コンパイラが drop を挿入する。

fun map(xs, f)
  match xs
    Cons(x, xx) -> ...
    Nil ->
      drop(f)             // f はこの分岐で不使用 → 即 drop
      Nil

原則: drop は可能な限り早く。 束縛の直後、最初の「使わない」ことが確定した地点で即座に挿入する。これにより RC==0 到達が早まり、メモリ消費のピークが下がる。

Rust のスコープベース解放と比較すると、Perceus はスコープ終了を待たず使用終了直後に解放するため、メモリ使用量がスコープベースの半分で済むケースがある。

Rule 3: recursive drop — 構造的再帰で子を解放

Rule 2 で RC==0 になった複合オブジェクトは、子フィールドそれぞれに対して再帰的に drop を適用してから自身を解放する。

// Rule 3 の素朴な実装
void drop_Cons(Cons* x) {
    if (is_unique(x)) {        // RC==1 → これが最後の参照
        drop(x->head);         // 子フィールドを再帰的に drop
        drop(x->tail);
        free(x);               // 自身を解放
    } else {
        rc_dec(x);             // RC>1 → カウントを減らすだけ
    }
}

この Rule 3 が drop specialization と reuse analysis の基盤になる。RC==1 の分岐で free(x) する代わりにメモリを再利用に回す発想が、Perceus の核心的な最適化を生む。

3ルールからの最適化

3ルール自体は正しくメモリを管理するだけだが、ここに2つの最適化を重ねることで性能が劇的に向上する。

Reuse Analysis

パターンマッチで分解されたオブジェクトと、同じ分岐で構築される新オブジェクトが同サイズなら、メモリを直接再利用する。

fun map(xs, f)
  match xs
    Cons(x, xx) ->
      val ru = if is_unique(xs) then &xs else NULL  // reuse token 発行
      dup(f)
      val y  = f(x)
      val ys = map(xx, f)
      if ru != NULL
        ru->head = y; ru->tail = ys  // in-place 再利用 (alloc なし)
      else
        alloc Cons(y, ys)            // 共有されているなら通常確保
    Nil ->
      drop(f)
      Nil

条件:

Drop Specialization

Rule 3 の recursive drop と reuse analysis を融合する最適化。free せずに reuse token として返す。

// Rule 3 そのまま (free する)
if (is_unique(x)) { drop(x.f1); drop(x.f2); free(x); }
else { decref(x); }

// Drop specialization (free せず reuse token として返す)
if (is_unique(x)) { drop(x.f1); drop(x.f2); return &x; }  // reuse token
else { decref(x); return NULL; }

fast path (RC==1) では free+alloc ペアが完全に消滅 する。これが Perceus を「Garbage Free」と呼ぶ根拠。

最適化の積み上げ

3ルール (正しさ)
  └─ Rule 1: dup → rc_inc
  └─ Rule 2: drop → rc_dec
  └─ Rule 3: recursive drop → 子を再帰的に drop + free
        │
        ├─ Reuse Analysis (効率)
        │    RC==1 なら同サイズオブジェクトのメモリを再利用
        │
        └─ Drop Specialization (融合)
             Rule 3 の free を reuse token 発行に置換
             → fast path で alloc も free も発生しない

Drop Insertion アルゴリズム

3ルールの dup/drop をコンパイラが自動配置するアルゴリズム。ANF (A-Normal Form) 上で動作する。

  1. プログラムを ANF に変換 (各部分式に名前を束縛)
  2. 各変数の「最後の使用地点」を構文的に確定
  3. dup: 変数が複数回使われる箇所で、2回目以降の使用前に挿入
  4. drop: 変数がある分岐で使われない場合、分岐の先頭で挿入

ANF のおかげで各変数の生存区間が構文的に明確になり、drop 位置を正確に決定できる。GC のような到達可能性解析は不要。

FBIP (Functional But In-Place)

Perceus の reuse analysis を前提にした設計パターン。Lorenzen, Leijen, Swierstra, ICFP’23 (“FP²”) で形式化。

キーワード保証スタックヒープ割当
fip完全 in-place定数ゼロ (引数がユニークなら)
fbipほぼ in-place非定数可ゼロ (解放は可)
// fip: alloc/free ゼロで動作することをコンパイラが保証
fip fun reverse-acc(xs : list<a>, acc : list<a>) : list<a>
  match xs
    Cons(x, xx) -> reverse-acc(xx, Cons(x, acc))  // Cons セルを in-place 再利用
    Nil -> acc

3ルール + reuse が正しく効いていれば、map, filter, ツリー走査 (Zipper パターン) が alloc ゼロで動作する。fip/fbip はその条件をコンパイラが静的にチェックするアノテーション。

Koka 言語

項目値
設計者Daan Leijen (Microsoft Research)
最新バージョンv3.2.3 (2026-03-17)
パラダイム関数型、Algebraic Effects + Handlers
コンパイルターゲットC11 → gcc/clang
メモリ管理Perceus (GC なし)
アロケータmimalloc
ライセンスApache 2.0

Algebraic Effects: 副作用を型レベルで追跡し、ハンドラで処理する。例外、async/await、非決定性を言語機能ではなくライブラリとして定義可能。

ベンチマーク

PLDI’21 の結果 (メモリ割当中心のベンチマーク):

第三者ベンチマーク (Pen 言語):

関連研究

研究関係
Lean 4 “Counting Immutable Beans” (IFL’19)Perceus に先行。借用参照で dup/drop を省略。Perceus が形式化と reuse analysis を強化
Frame Limited Reuse (Lorenzen & Leijen, ICFP’22)Perceus の reuse 解析の脆弱性を改善。drop-guided reuse でより堅牢に
FP² (ICFP’23)FBIP の形式化。fip/fbip キーワードによるコンパイル時保証
Swift ARCコンパイラ挿入型 RC だが reuse analysis なし。サイクル参照がありうる
Lobster所有権解析で RC 操作の約 95% を静的除去。reuse analysis は持たない

Perceus vs Rust の所有権

側面RustPerceus (Koka)
所有権コンパイル時に静的決定実行時 RC で追跡
解放タイミングスコープ終了時 (静的)使用終了直後 (RC==0 の瞬間)
メモリ使用量スコープベースで不必要に長保持する場合あり即座に解放、スコープベースの半分で済むケースも
プログラマ負担ライフタイム注釈、借用規則完全に透過的、注釈不要
RC オーバーヘッドなしあり (reuse/borrow で大幅削減)
reuseなし (ムーブセマンティクス)あり (自動 in-place 更新)

制限

制限詳細
サイクル参照Koka は純粋関数型なのでサイクルなし。ref を使うと可能だが手動破壊が必要
マルチスレッド共有データにはアトミック RC (relaxed ordering) が必要。非共有は非アトミックで高速
Reuse 不成立RC > 1 (共有)、サイズ不一致、中間参照保持時。通常の alloc/free にフォールバック
Fragility小さなリファクタリングで reuse が消失する可能性 (Frame Limited Reuse で一部改善)
短命大量オブジェクトGC の世代別回収が一括処理で効率的なケースがある。RC は個別 drop コスト

Region Inference との関係

側面PerceusRegion Inference
解放単位オブジェクト単位 (RC==0 で即解放)リージョン単位 (letregion 終了で一括解放)
再利用reuse token で同サイズオブジェクトを in-place 再利用リージョン内は bump allocator で再利用なし
空間リークRC==0 で即解放なのでリーク少ないLIFO 制約で不必要に長生きする問題あり
判定時点実行時 (RC チェック)コンパイル時 (型推論)
併用可能性bump allocator と共存可能。リージョン内で Perceus 的 reuse を行う構成が考えられる

押さえどころ(カード化候補)

関連

Graph