1. 程式人生 > >靜態分析之資料流分析與 SSA 入門 (二)

靜態分析之資料流分析與 SSA 入門 (二)

什麼是靜態單賦值 SSA

SSA 是 static single assignment 的縮寫,也就是靜態單賦值形式。顧名思義,就是每個變數只有唯一的賦值。

以下圖為例,左圖是原始程式碼,裡面有分支, y 變數在不同路徑中有不同賦值,最後列印 y 的值。右圖是等價的 SSA 形式,y 變數在兩個分支中被改寫為 y1, y2,在控制流交匯處插入 Ф 函式,合併了來自不同邊的 y1, y2 值, 賦給 y3, 最後列印的是 y3。

圖1 原始程式碼與 SSA 形式及相應 CFG 控制流圖

總結 SSA 形式的兩個特徵就是:

  1. 舊變數按照活動範圍(從變數的一次定義到使用)分割,被重新命名為新增數字編號字尾的新變數,每個變數只定義一次。
  2. 控制流交匯處有 Ф 函式將來自不同路徑的值合併。 Ф 函式表示一個 parallel 操作,根據執行的路徑選擇一個賦值。如果有多個 Ф 函式,它們是併發執行的。 

這裡引入兩個名詞 use-def chain 和 def-use chain。use-def chain 是一個數據結構,包含一個 def 變數,以及它的全部 use 的集合。相對的,def-use chain 包含一個 use 變數,以及它的全部 def 的集合。以圖2左圖為例,虛線就是 x 每處定義的 def-use chain. 傳統程式碼因為變數不止一次定義,所以每個定義的 def-use chain 非常複雜。再看右圖,SSA 形式下沒有同名變數,每個變數只定義一次,所以同名的 use 都是屬於它的 def-use chain.  而且因為每個變數 use 前都只有一次 def, 所以 use-def chain 是一對一的。可見,SSA 形式下的 def-use chain 與 use-def chain 都得到了簡化。

圖2 兩種形式下的 Def-use chain

SSA 形式的優點不僅在於簡化 def-use chain 與 use-def chain,它提供了一種稀疏表示的資料結構,極大方便了資料流分析。如圖3 所示,左邊是傳統的基於方程組的資料流分析,右邊是基於 SSA 形式的資料流分析。前文講過傳統資料流分析是在基本塊上沿控制流路徑或逆向迭代傳播,SSA 形式的 def-use chain 與 use-def chain 直接給出了更多資訊,資料流值傳播不侷限於控制流路徑。可見 SSA 形式可以簡化資料流分析。

圖3 迭代資料流分析與基於 SSA 形式的資料流分析

SSA 的幾種型別

SSA 有幾種不同風格

最小 SSA

最小靜態單賦值形式 (minimal SSA) 有以下特點:同一原始名字的兩個不同定義的路徑匯合處都插入一個 Ф 函式。這樣得到符合兩大特徵的且擁有最少 Ф 函式數量的 SSA 形式。但是這裡的最小不包含優化效果,比如死程式碼消除,或者值有可能經 live-range 分析是死(參考上篇資料流分析內容)的。

剪枝 SSA

如果變數在基本塊的入口處不是活躍 (live) 的,就不必插入 Ф 函式。一種方法是在插入 Ф 函式的時候計算活躍變數分析。另一種剪枝方式是在最小 SSA 上做死程式碼消除,刪掉多餘的 Ф 函式。

半剪枝 SSA

鑑於剪枝 SSA 的成本,可以稍微折衷一點。插入 Ф 函式前先去掉非跨越基本塊的變數名。這樣既減少了名字空間也沒有計算活躍變數集的開銷。

嚴格 SSA

首先引入一個名詞叫支配。如果從程式入口到一個結點 A 的所有路徑,都先經過結點 B,則稱 A 被 B 支配。如果 A 不等於 B,則稱 A 被 B 嚴格支配,A 的支配結點集記為 Dom(A),Dom(A) 中與 A 最接近的結點稱為直接支配結點,記為 IDom(A)。如下圖所示,B0 支配 B1,B1 支配 B2, B5... 支配關係是可傳遞的,例如 B0 也支配 B2, B5... 這樣可以根據支配關係構造一棵樹,路徑上的父結點直接支配子結點,根結點支配所有後代結點。

圖4 支配樹

如果一個 SSA 具有以下特點:每個 use 被其 def 支配,那麼稱為嚴格 SSA。如下圖所示,左圖 a, b 的 def 沒有支配 use,所以不是嚴格 SSA。在右圖中,匯合點插入2個 Ф 函式,重新編號了變數,保證了支配性。其中 ⊥ 表示未定義。

圖5 嚴格 SSA

傳統與變形 SSA

構造 Ф-web 並查集,將 def-use chains 有相同變數的都連線起來得到一個網路,比如 Ф 函式兩邊的 define 或 parameter 變數。可以得到若干網路。傳統 SSA 的特點是 Ф-web 中變數的 live-range 互相不干涉。傳統 SSA 經過優化例如複製傳播之後,可能會被破壞這個性質,就稱為變形 SSA。

SSA 構造演算法

這裡介紹一種基於支配邊界的 SSA 構造演算法。這個演算法分為兩個步驟:1,在支配邊界插入 Ф 結點;2,變數重新命名。 

計算支配邊界的目的是隻在需要的地方插入 Ф 結點,支配邊界的定義是:A 支配 B 的一個前驅但不嚴格支配 B,則稱 B 為 A 的支配邊界。A 的所有支配邊界組成的集合記為 DF(A)。DF 即 dominace frontier。

圖6 計算 DF 圖

計算支配邊界的方法如圖6. 左圖是控制流圖。首先構造出支配樹。中圖是一棵支配樹,從父結點到子結點的邊是支配邊。控制流路徑除了支配邊,就是匯合邊,例如 D 到 E,F 到 G。首先匯合邊的目的結點就是起點的支配邊界。例如 E 是 D 的支配邊界,F 是 G 的支配邊界。然後將匯合邊的起點向其直接支配點移動,例如 D 移動到 C,若 C 不是 E 的支配結點,則 E 也是 C 的支配邊界,以此類推。最右圖則表示最終計算結果,每個箭頭指向結點是起點的支配邊界。演算法偽碼如下:

```

for node in all nodes of CFG

    if n has multiple predecessors

        for each predecessor p of n

            runner = p

            while runner != IDom(n)

                add n to DF(runner)

                runner = IDom(runner)

```

計算完成支配邊界後基本塊 b 中對 x 定義,則在 DF(b) 內每個結點起始處放置一個 Ф 函式。放置過 Ф 函式的基本塊其 DF 也要繼續放置 Ф 函式。

最後是變數重新命名。

每個全域性名(對應半剪枝型別,即不考慮不跨越基本塊的變數)變成一個基本名,對其各個定義新增數字編號。例如 x,對第一個定義命名為 x1,第二個定義命名為 x2... 方法是在支配樹上先序遍歷,在每個塊上首先重新命名 Ф 函式的定義,然後依次訪問各條指令,用當前 SSA 名重寫各運算元,併為操作的結果建立一個新的 SSA 名。然後使用當前 SSA 名改寫後繼塊 Ф 函式的引數。最後對支配樹的子結點遞迴處理。返回後將 SSA 名恢復前一個狀態。所以這裡可以用每變數一個棧來存放 SSA 名,壓棧時 SSA 名的編號遞增,處理完一個基本塊後本塊內生成名全部出棧。 虛擬碼如下:

```

NewName(n)

    i = counter[n]

    counter[n] ++

    push i onto stack[n]

    return "ni"

Rename(b)

    for each Ф-function "x = Ф(...)", rewrite x as NewName(x)

    for each operation "x = y op z", rewrite y as top(stack[y]), rewrite z as top(stack[z]), rewrite x as NewName(x)

    for each successor of b in CFG, fill in Ф-funcitons

    for each successor s of b in dominator tree, Rename(s)

    popstack[x]

``` 

SSA 解構演算法

處理器不能處理 Ф 函式,所以我們需要將 SSA 形式轉換回可執行程式碼,這就是 SSA 解構。

SSA 解構是去掉 Ф 函式的操作,但是需要增加一些複製。例如  xi = Ф(xj, xk),去掉這一條語句後應該沿著傳入 xj 的邊插入 xi = xj,沿著傳入 xk 的邊插入 xi = xk,就能保證執行正確。但這裡面有幾種特殊情況:

關鍵邊 critical edge

邊的源結點有多個後繼,目標結點有多個前驅,則稱為關鍵邊。這種情況需要拆分關鍵邊,在關鍵邊裡面增加一個基本塊,並將複製操作放在這個基本塊裡。以下圖為例。圖 (a) 到 圖 (b) 是構造 SSA 後進行復制摺疊的優化。圖 (c) 是插入複製不正確的例子。i1 的 Ф 刪除後,在前繼塊插入複製。但是另一條後繼路徑上給 z0 賦值時 i1 的值卻不正確。如果按照圖 (d) 所示,拆分關鍵邊後複製就是正確的。

圖7 關鍵邊拆分

丟失複製問題 lost-copy

仍然以圖7 為例,如果無法拆分關鍵邊。那麼就出現了圖 (c) 的複製丟失問題。有一種解法是分析複製操作的目標,如果是活動狀態,則建立一個臨時變數,重寫後續引用。例如圖 (e) 所示。

交換問題 swap

 在語義上要求同一基本塊內的 Ф 函式併發執行。圖 (c) 是解構 SSA 後插入複製操作的結果。因為操作不是併發而是順序執行的,x1, y1 的交換失敗了。這個問題的解法是分析 Ф 函式有沒有引用同一基本塊其他 Ф 函式的結果,對於引用形成的環則必須插入臨時變數。

圖8 交換問題

這些特殊 case 其實並不容易遇到,往往是複製摺疊優化等程式碼被重排和改變的操作之後才會出現。如果是新建的 SSA,則不會有這種問題。

以上是對 SSA 演算法的簡單總結。例子與圖引用自《static single assignment book》與 《engineering a compiler》。