暫存器配置

在編譯器最佳化的領域裡，**暫存器組態**（Register Allocation）的用途，在於使一個在較少暫存器數量的 CPU 可使用較大數量的變數，暫存器組態可使用在一個**基本區段（Basic block）** （**區域暫存器組態**）、函式或程式（**全域暫存器組態**）、或是透過 Call Graph 進行跨函式邊域分析（**跨程式暫存器組態**），當完成每個函式或是程式，慣例上會要求每個呼叫函式的位置（Call site）必須插入儲存或是還原。

## 簡介

許多程式語言，程式設計師會有任意地組態過多變數的錯誤觀念，然而在編譯時，編譯器必須決定在一個較小及有限暫存器的系統中如何分配這些變數，並非所有變數都是在同一時間執行，所以有些暫存器可能被分配超過一個變數。然而，兩個被分配在同一暫存器的變數，若在同一時間使用，若是不破壞他的數值就無法被分配在相同的暫存器。無法被分配在相同的暫存器的變數必須被保留在隨機存取記憶體，在需要讀取或寫入時才會被載入，這個過程稱之為**溢位**（spilling）。記憶體存取速度比存取暫存器還慢，這會降低程式的執行速度，所以一個最佳化的編譯器會盡可能的將更多的變數放置在暫存器。_暫存器壓力_（Register pressure）這個詞被使用在當硬體暫存器數量比起理想數量還少的狀況，高壓的情況通常代表會產生更多溢位以及更多重載的情況發生。

除此之外，程式可以被進一步的最佳化，只要可行，任何地方都能透過 `move` 指令來使一個暫存器的數值可以被移進移出，這在編譯器中相當重要，這被使用在一些最佳化技術，例如靜態單賦值形式，他會在中間碼額外產生 `move` 指令。

## 圖著色性的同構

透過活躍變數分析（Live variable analysis），編譯器可以決定哪個變數的集合在同一時間是活躍的，也就是涉入 `move` 指令的變數。使用這些資訊，編譯器可以建構一張圖，使每個點（Vertex）在程式中代表一個獨立的變數。當變數被同時使用時，則利用_干擾邊_（Interference edges）連結兩個節點，當變數同時涉入 move 指令時，則建立_優先邊_（preference edges）。可以透過 K-coloring 用來解決暫存器組態的問題（K 為暫存器可用的數量）。兩個共享一個干擾邊的節點不會被分配相同的顏色，而共享一個優先邊的節點可能會被分配相同的顏色，有些節點可能一開始就會被上色，代表這些變數因為慣例或是模組間溝通的因素而必須留在某些特定的暫存器，圖著色問題廣義來說是 NP 完全問題，然而一個好的演算法必須平衡效能以及程式碼的品質。

圖著色技術是相當有效率，因為它不只考慮到變數的暫存器組態，還考慮在同時間執行的變數，邏輯上，如果變數 V 所有活躍的鄰近變數可以被組態暫存器，那麼通過 V 則可以存取到所有鄰近的變數，所以這是一個遞迴的案例，目的是移除活躍變數集合中的變數。這個迴圈將持續進行，直到所有活躍變數都可以被組態，而其他的變數則溢位到記憶體。

## 溢位

在多數的暫存器分配，每個變數會存在暫存器或是記憶體，換句話說，如果一個變數無法被分配到一個暫存器，那麼當這個變數要被使用時就會從記憶體載入。一個_溢位的變數_（Spilled Variable）代表一個變數在記憶體中而不在 CPU 的暫存器。舉例來說，一個 32 位元的變數溢位到記憶體，他會取得 32 位元的堆疊空間，而所有使用到這個變數的位置將會指到記憶體，這樣的變數的處理速度相較於暫存器的變數就會比較慢，所以要決定哪些變數必須溢位，就必須考慮到執行時間、程式碼佔用空間以及資料空間等因素。

## 迭代暫存器接合

暫存器分配有很多類型，迭代暫存器接合（Iterated Register Coalescing，IRC）則是常用的一種，IRC 是由 LAL George 及 Andrew Appel 在 1996 年提出，IRC 基於一些原理所運作，第一，如果在圖中存在無法被移動的節點，而這些與這些節點的連接的數量小於 K，則這些圖可以直接移除掉這些節點，因為一旦這些節點被加回去，則可以保證找到他們的顏色（簡化）。第二，兩個節點共享一個優先邊，而他們鄰近集合的連接總數小於 K，那麼這兩個點可以被結合為一個節點（接合），如果上述兩個步驟可以簡化圖，簡化的程式在移動相關節點時（凍結時），可以再執行一次。最後，如果沒有任何其他工作了，節點可以被標示為可能溢位並且從圖中移除（溢位）。以上步驟用以減少圖中節點的連接數，節點的連接數可能從大於 K 的情況降為低連接數，使節點可以被簡化或是接合。這個階段的演算法被迭代，以確保簡化及接合的品質。虛擬程式碼如下：

```
 function IRC_color g K :
 repeat
   if ∃v s.t. !moveRelated(v) ∧ degree(v) < K then simplify v
   else if ∃e s.t. cardinality(neighbors(first e) ∪ neighbors(second e)) < K then coalesce e
   else if ∃v s.t. moveRelated(v) then deletePreferenceEdges v
   else if ∃v s.t. !precolored(v) then spill v
   else return
 loop
```

在 IRC 中進行接合是相當穩定的，因為一個積極的接合可能會導致圖的溢位，然而這同時啟發了像是 George coalescing 接合了更多的節點，更可確保沒有額外的溢位發生，Work-lists 被使用在這個演算法來確保每個 IRC 的迭代皆需要 sub-quadratic time。

## 近期發展

利用圖著色來改進的程式碼的效率雖然有效，但是組態的時間仍然很長，在靜態編譯的案例中，組態時間並不是那麼被在意，但是在動態編譯的案例中，例如即時編譯（Just-in-time，JIT）編譯器，快速的暫存器組態是很重要的，Poletto 及 Sarkar 提出一個的有效技術線性掃描組態 （頁面存檔備份，存於網際網路檔案館），這個技術僅需要一個階段取得活躍變數的區間列表，較短生命週期區間的變數將會被分配到暫存器，而較長生命週期的變數將會被溢位，這個結果的效能平均只比圖著色降低 12%。

線性掃瞄演算法的步驟如下：

1. 執行資料流程分析，用以取得活躍度資訊。持續紀錄所有變數的活躍間隔於一個列表並以遞減排序，這個間隔代表在這段時間內變數是活躍的，我們認為變數以及他們的間隔在這個演算法中是可以交換的。
2. 反覆通過活躍度的開始點，並且組態一個暫存器給每個活躍的變數
3. 在每個步驟維護一張在間隔內運作的列表，以活躍間隔的結束點進行排序（注意到插入有序的節點到一個平衡的二元樹，可使這張列表的維護成本為線性成本）。移除所有的逾期間隔，並且釋放這些逾期的暫存器。
4. 當列表大小 R，我們無法組態暫存器時，從運作列表中溢位間隔距離最遠的點，並且分配給目前的間隔。如果目前的間隔是一個溢位的間隔，則不改變暫存器的分配。

Cooper 及 Dasgupta 近期開發了一個_有損的_ Chaitin-Briggs 圖著色演算法，適用於 JIT。_有損_代表這個演算法所提出的干擾圖並不是那麼精確，這個最佳化減少了圖建立的步驟，Chaitin-Briggs 使它適合執行時期的編譯。實驗中顯示，這個有損的暫存器組態比起線性掃瞄效率來得好。

_理想的_暫存器組態演算法是基於 Goodwin 及 Wilken 所開發的線性規劃演算法，這些演算法已經被 Kong 及 Wilken 延伸到更多架構。

最糟的情況是執行時間為指數，這個實驗結果顯示，實際的時間是典型的 $O(n^{2.5})$ 。

在靜態單賦值形式進行暫存器組態的可能，是近期研究所專注的項目，SSA 形式程式的干擾圖為弦圖，可在多項式時間內進行著色。

## 參見

* Strahler number, the minimum number of registers needed to evaluate an expression tree.

## 參考文獻