TT Lab
はじめる
学ぶ 学習パス コース

コンパイラ — 小さな言語を最初から最後まで作る

線形スキャンでレジスタを分け、マークしてスイープする

TT Labで続きを見る

目標

仮想レジスターを使う中間コードで、値が生きている区間を求めて、線形スキャンで実際のレジスターk個に割り当て、ループのあるCFGの生存解析までを行います。続けて、ヒープのグラフでルートに届くオブジェクトだけを残すmark-sweepコレクターを作り、参照カウント方式が何を見逃すかを数えます。

なぜ重要なのか

-O0と-O2のいちばん大きな違いは、値をレジスターに置くかどうかで、その決定は「2つの値が同時に生きているか」にかかっています。ガベージコレクションは、その反対側、つまり実行中に作ったオブジェクトをいつ捨ててよいかを、同じ道具(グラフをたどって届くものを探す)で解きます。どちらも、規則の1文字(<と<=、再帰と反復)で間違えやすいものです。

ルール

중간 코드   명령 {"op", "def"(없을 수 있음), "use": [..]} 의 목록. 가상 레지스터마다 def 는 한 번(param 도 def)
liveness    명령마다 그 명령 **바로 뒤** 에 살아 있는 값(이름순 목록). 마지막 명령 뒤는 빈 목록
intervals   {값: [정의한 명령, 마지막으로 쓰는 명령]} (쓰지 않으면 [정의, 정의])
max_pressure 한 명령에서 쓰는 값 ∪ 정의하는 값 ∪ 그 뒤에 살아 있는 값 의 크기 가운데 최댓값
linear_scan(구간, k)  → {값: "r0".."r{k-1}" 또는 "spill"}
            시작 순(같으면 이름순)으로 훑는다. 새 구간 앞에서 끝 < 새 시작 인 활성 구간만 풀어 준다.
            빈 레지스터가 있으면 번호가 가장 작은 것. 없으면 활성 구간과 새 구간 가운데 끝이 가장 먼 것
            (끝이 같으면 이름이 큰 쪽)을 넘기고, 넘긴 것이 활성 구간이면 그 레지스터를 새 구간이 물려받는다
conflicts   같은 레지스터를 받은 두 구간이 겹치면(s1 ≤ e2 이고 s2 ≤ e1) [a, b](이름순), 목록도 정렬
block_liveness(blocks, succ)  → (live_in, live_out) 블록마다 이름순 목록. 고정점까지 되풀이
mark(heap, roots)   heap = {번호: [가리키는 번호…]}. 뿌리에서 닿는 번호(오름차순). 힙에 없는 뿌리는 무시.
                    재귀하지 않는다(10만 개짜리 연결 리스트도 받는다)
sweep(heap, marked) → (살아남은 힙, 치운 번호 오름차순)
refcount_leaks(heap, roots)  뿌리도 참조 하나로 센다. 0 인 것을 치우며 가리키던 것의 수를 줄이고 되풀이.
                    그래도 남았는데 뿌리에서 닿지 않는 번호(오름차순)
simulate(trace, capacity)  기록: ["alloc", n] · ["link", a, b] · ["unlink", a, b] · ["root", n] · ["unroot", n]
                    alloc 하려는데 힙이 capacity 개면 먼저 mark-sweep. 그래도 꽉 차면 "out of memory" 로 멈춘다.
                    → {"collections", "freed", "peak", "live"(끝의 번호 오름차순), "error"}

ステップ

  1. /root/mini/regalloc.pyのliveness(code)を埋めます。
  2. intervals(code)とmax_pressure(code)を埋めます。
  3. linear_scan(ivs, k)を埋めます。採点ツールが、レジスター1・2・3・4・6個で突き合わせます。
  4. conflicts(ivs, alloc)を埋めます。正しい割り当てで誤検知が出ないようにします。
  5. block_liveness(blocks, succ)を埋めます。/opt/fixtures/mini/regalloc/loop.jsonに、ループの例があります。
  6. /root/mini/heap.pyのmark(heap, roots)を埋めます。
  7. sweep(heap, marked)とrefcount_leaks(heap, roots)を埋めます。
  8. simulate(trace, capacity)を埋めます。/opt/fixtures/mini/regalloc/trace.jsonが、手でたどってみられる例です。

参考

後ろから前へ: 生存解析

liveの集合を空にしておき、最後の命令から逆にたどります。命令ごとに、まず今のliveをその命令のlive-outとして書き、そのあとdefを引いてuseを足します(それが前の命令のlive-outになります)。

生存区間と同時に生きている数

defに出会ったら[i, i]で始め、useに出会うたびに、終わりをmax(終わり, i)に伸ばします。同時に生きている値の最大数は、命令ごとにuse・def・live-outを合わせた集合の大きさのうち、いちばん大きい値です。

線形スキャン

開始の順にたどりながら、アクティブな区間のリストを持ち歩きます。新しい区間ごとに、まず終わり < 新しい開始のものだけを解放して、レジスターを返してもらいます(同じならまだ使用中です)。空いているレジスターは、番号の小さいものから。なければ(終わり, 名前)がいちばん大きいものを退避させ、それが新しい区間でなければ、そのレジスターを新しい区間に渡します。

割り当てを検証する

spillでない値を名前順に並べて、すべての組を見ます。同じレジスターで、区間が重なれば(一方の開始 ≤ 他方の終わり、そして逆も)衝突です。終わりと開始が接しているものも重なりです。その命令が両方を使うからです。

ループのある生存解析

ブロックごとに、use(ブロックの中で定義される前に使われたもの)とdefを先に求めます。そのあと、live_out = 後続ブロックのlive_inの和集合、live_in = use ∪ (live_out − def)を、すべてのブロックに適用することを、1周のあいだ何も変わらなくなるまで繰り返します。

ルートから届くものにマークを付ける

再帰の代わりに、リストをスタックとして使います。ヒープにあるルートから始めて、取り出したオブジェクトがすでに見たものなら飛ばし、そうでなければマークを付けて、それが指しているオブジェクトたちを積みます。すでに見たものを飛ばすことで、循環でも終わります。

スイープ、そして参照カウントが見逃すもの

sweepは、マークされたものだけを新しい辞書に移します。refcount_leaksは、すべてのオブジェクトの入ってくる参照の数(ルートを含む)を数え、0のものを片付けながら、それが指していたものの数を減らすことを繰り返します。最後まで残ったのにmarkで届かないものが、漏れている分です。

割り当て記録をたどって動くコレクター

allocに出会ったときにすでにcapacity個なら、mark → sweepしたあと、収集の回数と片付けた数を足します。それでもcapacity個なら、errorを書いて止まります。peakは、alloc直後のオブジェクト数の最大値です。trace.jsonを手でたどってみると、規則が見えます。