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

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

レジスタは少なく値は多い — 寿命で分け、片付ける

TT Labで続きを見る

一言でいうと

コンパイラーは、値ごとに名前(仮想レジスター)を際限なく作って使いますが、CPUのレジスターは16個しかありません。生存解析で値がいつからいつまで生きているかを求め、線形スキャンで重ならない値どうしに同じレジスターを分け合わせ、足りなければ一部をメモリへ退避します(spill)。プログラムが実行中に作ったオブジェクトは、反対に、だれからも指されなくなったら、ガベージコレクターがルートから届くものだけを残して片付けます。

なぜ必要なのか

モジュール8のコード生成器は、すべてのローカル変数をスタックスロットに置き、使うたびにメモリから読み書きします。間違いではありませんが、レジスターは1サイクルで読めるのに対し、メモリはキャッシュにあっても数サイクルかかります。gcc -O0と-O2の出力の違いのうち、いちばん大きな部分がこれ、つまり値をレジスターに置くかどうかです。

ところが、値は多く、レジスターは少ししかありません。2つの値が同時に生きていないなら、同じレジスターを使ってかまいません。「生きている」とは「この値をあとでもう一度読むことがある」という意味で、それをコードだけを見て計算するのが、生存解析です。

メモリの反対側にも、同じ問いがあります。モジュール5のクロージャは、呼び出しが終わった環境をつかんで生かしておきました。では、いつ捨ててもよいのでしょうか。だれにも届かなくなったときです。ガベージコレクターは、それを人の代わりに計算します。

どう動くのか

生存解析は後ろから前へ。命令iの直後に生きている値(live-out)がわかれば、その前(live-in)はuse ∪ (live_out − def)です。この命令が使う値は前でも生きている必要があり、この命令が定義する値は前にはまだありませんでした。最後の命令から逆にたどれば、1回で終わります。ループのあるCFGでは、後ろへ戻る辺のせいで1回では終わりません。ブロック単位でlive_out = 다음 블록들의 live_in 합집합(プレースホルダーは次のブロックたちのlive_inの和集合です)を、もう変わらなくなるまで繰り返します(モジュール7のドミネーターと同じ固定点計算です)。

線形スキャン。値ごとに[定義した命令, 最後に使う命令]の区間を作り、開始の順にたどりながらレジスターを割り当てます。

구간(명령 번호)          k = 2 개의 레지스터로
t1 [0 ──────── 4]        t1 → r0
t2   [1 ───────────── 6] t2 → r1
t3     [2 ──── 4]        t3: 빈 레지스터가 없다. 활성인 t1(끝 4)·t2(끝 6)와 t3(끝 4) 가운데
                             끝이 가장 먼 t2 를 넘기고, t3 가 t2 의 r1 을 물려받는다
t4        [3 ── 5]       t4: 여전히 꽉 참. t1·t3(끝 4)·t4(끝 5) 가운데 끝이 가장 먼 것은 t4 자신 → t4 를 넘긴다
t5          [4 ─ 5]      t5: 시작 4 — 끝이 4 인 t1·t3 는 아직 쓰는 중(같으면 풀지 않는다) → 넘긴다

終わりがいちばん遠いものを退避させる理由は、その値がレジスターをいちばん長く握り続けるからです。退避した値は、使うたびにメモリから読み書きします(モジュール8のスロットが、まさにその場所です)。新しい区間の前では、終わりが新しい開始より小さい区間だけを解放します。同じ命令で終わる値は、その命令がまだ読んでいる最中だからです。この1文字の違い(<と<=)が、いちばんよくある間違いです。

mark-sweep。ヒープを「オブジェクト → 指しているオブジェクトたち」のグラフと見ると、ルート(スタックとグローバルにある値)からたどって届くオブジェクトだけが生きています。マーク(mark)の段階は、ルートからグラフをたどって届くものに印を付け、スイープ(sweep)の段階は、印のないものをすべて片付けます。循環(A ↔ B)があっても、すでに印を付けたものは見直さないので終わり、ルートから切り離された循環は、丸ごと片付けられます。マークを再帰で書くと、10万個の連結リストで再帰の上限を超えるので、明示的なスタックでたどります。

参照カウントが見逃すもの。オブジェクトごとに「何か所が自分を指しているか」を数えて、0になったらすぐ片付ける方式(参照カウント)は、マークの段階なしにすぐ片付けるので、停止がありません。ただし、AとBが互いを指していると、ルートから切り離されても、数が1から下がらず、永遠に残ります。CPythonが参照カウントを基本にしながら、循環を探すコレクター(gcモジュール)を別に持っている理由です。

現場での姿

次のラボですること

regalloc.pyに、直線コードの生存解析、生存区間と同時に生きている値の最大数、線形スキャン、割り当ての検証器、ループのあるCFGのブロックごとの生存解析を作り、heap.pyに、マーク・スイープ、参照カウントが見逃すゴミの計算、割り当て記録をたどって動くコレクターを作ります。採点ツールは、ランダムなコード・グラフ・ヒープ・記録数百個で基準と突き合わせます。