本章是系列文章的第四章,介紹了worklist演算法。Worklist演算法是圖分析的核心演算法,可以說學會了worklist演算法,編譯器的優化方法才算入門。這章學習起來比較吃力,想要融匯貫通的同學,建議多參考幾個學校的教程交叉著看。
卡耐基梅隆大學 15745: https://www.cs.cmu.edu/afs/cs/academic/class/15745-s16/www/lectures/L5-Intro-to-Dataflow.pdf
密西根大學 583f18: http://web.eecs.umich.edu/~mahlke/courses/583f18/lectures/583L5.pdf 和 http://web.eecs.umich.edu/~mahlke/courses/583f18/lectures/583L6.pdf
哈工大 編譯原理:編譯原理 - 哈爾濱工業大學_嗶哩嗶哩_bilibili
但密西根大學的583f18根本就沒說worklist演算法,只是簡單的資料流分析,卡耐基梅隆大學15745提到了worklist,但也講的非常簡單,還是DCC888裡面講的稍微詳細一點。
本文中的所有內容來自學習DCC888的學習筆記或者自己理解的整理,如需轉載請註明出處。周榮華@燧原科技
別人說知識爆炸,我們先做知識轟炸,說不定哪天就把腦子炸開了。
4.1 解析約束
- 資料流分析的本質是建立約束系統
- 約束系統是對問題的高階抽象
- 解析約束系統有很多有效的演算法
- 對待解決問題的描述同樣也提供了一種解決方案
- 怎麼實際的解析一種約束系統?效能上可行並且有很大概率是正確的演算法
4.1.1 使用prolog進行約束解析
手頭沒有prolog的同學可以從下面連結裡面下載一個win64的版本:
https://www.swi-prolog.org/download/stable/bin/swipl-8.4.2-1.x64.exe
如果喜歡linux的也可以在安裝一個linux的版本,例如ubuntu的下載命令:
apt install gprolog
安裝完之後執行swipl可以進入swipl的prolog介面(linux下面安裝的是gprolog,所以執行gprolog)。prolog的每條命令都用“.”結束,例如啟動之後可以敲“pwd.”看到當前目錄,退出可以敲“halt.”。
給定程式,這裡面有幾個BB基本塊?
1 if b1 2 then 3 while b2 do x = a1 4 else 5 while b3 do x = a2 6 x = a3
BB列表:
1: b1 2: b2 3: x=a1 4: b3 5: x=a2 6: x=a3
通過BB分析畫出CFG:
1 digraph { 2 "1: b1" -> {"2: b2" "4: b3"} 3 "2: b2" -> "3: x=a1" -> "2: b2" 4 "4: b3" -> "5: x=a2" -> "4: b3" 5 {"2: b2" "4: b3"} -> "6: x=a3" 6 }
我們複習一下上一章的reaching definition,上面這個例程的reaching definition推導過程如下:
1 IN[x1] = {} 2 IN[x2] = OUT[x1] ∪ OUT[x3] 3 IN[x3] = OUT[x2] 4 IN[x4] = OUT[x1] ∪ OUT[x5] 5 IN[x5] = OUT[x4] 6 IN[x6] = OUT[x2] ∪ OUT[x4] 7 OUT[x1] = IN[x1] 8 OUT[x2] = IN[x2] 9 OUT[x3] = (IN[x3]\{3,5,6}) ∪ {3} 10 OUT[x4] = IN[x4] 11 OUT[x5] = (IN[x5]\{3,5,6}) ∪ {5} 12 OUT[x6] = (IN[x6]\{3,5,6}) ∪ {6}
我們將這裡的過程整理成prolog程式碼:
1 diff([], _, []). 2 diff([H|T], L, LL) :- member(H, L), diff(T, L, LL). 3 diff([H|T], L, [H|LL]) 4 :- \+ member(H, L), diff(T, L, LL). 5 union([], L, L). 6 union([H|T], L, [H|LL]) 7 :- union(T, L, LL). 8 9 10 solution([X1_IN, X2_IN, X3_IN, X4_IN, X5_IN, X6_IN, 11 X1_OUT, X2_OUT, X3_OUT, X4_OUT, X5_OUT, X6_OUT]) :- 12 X1_IN = [], 13 union(X1_OUT, X3_OUT, X2_IN), 14 X3_IN = X2_OUT, 15 union(X1_OUT, X5_OUT, X4_IN), 16 X5_IN = X4_OUT, 17 union(X2_OUT, X4_OUT, X6_IN), 18 X1_OUT = X1_IN, 19 X2_OUT = X2_IN, 20 diff(X3_IN, [3, 5, 6], XA), union(XA, [3], X3_OUT), 21 X4_OUT = X4_IN, 22 diff(X5_IN, [3, 5, 6], XB), union(XB, [5], X5_OUT), 23 diff(X6_IN, [3, 5, 6], XC), union(XC, [6], X6_OUT), !.
prolog的語法可以參考swi-prolog的手冊manual (swi-prolog.org),注意swi的prolog自己擴充套件了一些prolog函式,有些函式在其他版本的prolog裡面不一定能用。
將上面的prolog程式碼儲存成一個pl檔案,例如rd.pl,執行swipl(linux 下是gprolog),載入並執行,可以得到
solution([[], [3], [3], [5], [5], [3, 5], [], [3], [3], [5], [5], [6]]) 返回值是true(gprolog返回yes)。如果想知道推導過程,可以用“trace.”開啟單步執行:
1 PS D:\doc\DC888\hw> swipl 2 3 1 ?- [rd]. 4 true. 5 6 2 ?- solution([[], [3], [3], [5], [5], [3, 5], [], [3], [3], [5], [5], [6]]). 7 true. 8 9 3 ?- trace. 10 true. 11 12 [trace] 3 ?- solution([[], [3], [3], [5], [5], [3, 5], [], [3], [3], [5], [5], [6]]). 13 Call: (10) solution([[], [3], [3], [5], [5], [3, 5], [], [...]|...]) ? creep 14 Call: (11) []=[] ? creep 15 Exit: (11) []=[] ? creep 16 Call: (11) union([], [3], [3]) ? creep 17 Exit: (11) union([], [3], [3]) ? creep 18 Call: (11) [3]=[3] ? creep 19 Exit: (11) [3]=[3] ? creep 20 Call: (11) union([], [5], [5]) ? creep 21 Exit: (11) union([], [5], [5]) ? creep 22 Call: (11) [5]=[5] ? creep 23 Exit: (11) [5]=[5] ? creep 24 Call: (11) union([3], [5], [3, 5]) ? creep 25 Call: (12) lists:member(3, [5]) ? creep 26 Fail: (12) lists:member(3, [5]) ? creep 27 Redo: (11) union([3], [5], [3, 5]) ? creep 28 Call: (12) lists:member(3, [5]) ? creep 29 Fail: (12) lists:member(3, [5]) ? creep 30 Redo: (11) union([3], [5], [3, 5]) ? creep 31 Call: (12) union([], [5], [5]) ? creep 32 Exit: (12) union([], [5], [5]) ? creep 33 Exit: (11) union([3], [5], [3, 5]) ? creep 34 Call: (11) []=[] ? creep 35 Exit: (11) []=[] ? creep 36 Call: (11) [3]=[3] ? creep 37 Exit: (11) [3]=[3] ? creep 38 Call: (11) diff([3], [3, 5, 6], _20084) ? creep 39 Call: (12) lists:member(3, [3, 5, 6]) ? creep 40 Exit: (12) lists:member(3, [3, 5, 6]) ? creep 41 Call: (12) diff([], [3, 5, 6], _20084) ? creep 42 Exit: (12) diff([], [3, 5, 6], []) ? creep 43 Exit: (11) diff([3], [3, 5, 6], []) ? creep 44 Call: (11) union([], [3], [3]) ? creep 45 Exit: (11) union([], [3], [3]) ? creep 46 Call: (11) [5]=[5] ? creep 47 Exit: (11) [5]=[5] ? creep 48 Call: (11) diff([5], [3, 5, 6], _27698) ? creep 49 Call: (12) lists:member(5, [3, 5, 6]) ? creep 50 Exit: (12) lists:member(5, [3, 5, 6]) ? creep 51 Call: (12) diff([], [3, 5, 6], _27698) ? creep 52 Exit: (12) diff([], [3, 5, 6], []) ? creep 53 Exit: (11) diff([5], [3, 5, 6], []) ? creep 54 Call: (11) union([], [5], [5]) ? creep 55 Exit: (11) union([], [5], [5]) ? creep 56 Call: (11) diff([3, 5], [3, 5, 6], _2514) ? creep 57 Call: (12) lists:member(3, [3, 5, 6]) ? creep 58 Exit: (12) lists:member(3, [3, 5, 6]) ? creep 59 Call: (12) diff([5], [3, 5, 6], _2514) ? creep 60 Call: (13) lists:member(5, [3, 5, 6]) ? creep 61 Exit: (13) lists:member(5, [3, 5, 6]) ? creep 62 Call: (13) diff([], [3, 5, 6], _2514) ? creep 63 Exit: (13) diff([], [3, 5, 6], []) ? creep 64 Exit: (12) diff([5], [3, 5, 6], []) ? creep 65 Exit: (11) diff([3, 5], [3, 5, 6], []) ? creep 66 Call: (11) union([], [6], [6]) ? creep 67 Exit: (11) union([], [6], [6]) ? creep 68 Exit: (10) solution([[], [3], [3], [5], [5], [3, 5], [], [...]|...]) ? creep 69 true.
solution([[], [3], [3], [5], [5], [3, 5], [], [3], [3], [5], [5], [6]])執行返回true表示,1~6處的reaching definition的輸入集合分別為:
[], [3], [3], [5], [5], [3, 5],
輸出集合分別為:
[], [3], [3], [5], [5], [6]
的情況下,是能被驗證成功的。
但驗證通過的集合不一定是最優解,例如,如果在一些集合中加入4,一樣也能推導成功:
solution([[], [3], [3], [4, 5], [4, 5], [3, 4, 5], [], [3], [3], [4, 5], [4, 5], [4, 6]]).
一般的,把這些4換成任意變數也是能成功的:
1 PS D:\doc\DC888\hw> swipl 2 3 1 ?- [rd]. 4 true. 5 6 2 ?- solution([[], [3], [3], [a, 5], [a, 5], [3, a, 5], [], [3], [3], [a, 5], [a, 5], [a, 6]]). 7 true. 8 9 3 ?-
這主要是因為集合的diff和union函式中有一個特殊處理,下面2個函式的意思是,不論H是否是L的成員diff([H|T], L, [H|LL])和diff(T, L, LL)等價,union([H|T], L, [H|LL])和union(T, L, LL)等價,這對一些未知元素的推導會非常有用:
1 diff([H|T], L, [H|LL]) 2 :- \+ member(H, L), diff(T, L, LL). 3 union([H|T], L, [H|LL]) 4 :- \+ member(H, L), union(T, L, LL). 5 ji
就實際意義而言,程式點4或者程式點2處,如果新定義了變數,則可以傳遞到後面程式點的reaching definition集合裡面,如果沒有定義任何變數(例如當前的例程只是做了true和false的判斷),那後面就沒有程式點4或者程式點2處新增的變數集合。
雖然這個推導能容忍不同的答案,但還是能保證一致性,如果將X6_OUT換成空,則程式會報false:
1 PS D:\doc\DC888\hw> swipl 2 3 1 ?- [rd]. 4 true. 5 6 2 ?- solution([[], [3], [3], [a, 5], [a, 5], [3, a, 5], [], [3], [3], [a, 5], [a, 5], []]). 7 false. 8 9 3 ?-
因為不論前面的輸入或者輸出集合怎麼變化,在程式點6新定義了變數x,這個新增的變數x肯定是能進入程式點6的reaching definition的輸出集合的。
4.1.2 靜態分析和動態分析結果
靜態分析有時候會提供一些在實際執行中不可能發生的結果。
例如對下面的CFG,由於y是x的平方,肯定不會小於0,所以程式點4肯定執行不到。
如果實際執行中某個定義D在基本塊B處可達,但靜態分析推匯出來的結果是不可達,則稱為這個靜態分析是假的不可達(false negative,是不是有點像核酸檢測中的假陰?)。假的不可達是錯誤結論。如果核酸檢測中某個人本來是陽性,核酸檢測結果是陰性,那後面可能沒人會對他進行復核,導致這個陽性流出到社會面。所以假陰的後果是很嚴重的。
相對的,如果靜態分析的結果認為某個定義D在基本塊B處可達,但實際執行過程中不可達,則認為這個靜態分析是假的可達(false positive,類似核酸檢測的假陽)。假的可達是不嚴密的(imprecise),但不能算錯誤。例如核酸檢測中出現假陽,後面會專門進行復核,如果多次複核之後發現是假陽,之前的檢測結果可以取消掉。
4.1.3 使用prolog找到解決方案
前面prolog執行過程主要都是檢查某個解決方案是否正確,但實際上也可以用prolog找到解決方案。
但要注意,prolog是對系統空間的窮舉計算(類似tla+,Temporal Logic of Actions,參見The TLA+ Home Page (lamport.azurewebsites.net)),所以有些推導可能是非常耗時,甚至由於中間存在迴圈,可能永遠無法結束。
還是上面的例子,如果執行下面的命令,在windows下面會一直迴圈下去,在linux下執行會提示堆疊溢位,加了“length(X5_OUT, 1), ”限制就能正常跑出結果:
solution([X1_IN, X2_IN, X3_IN, X4_IN, X5_IN, X6_IN, X1_OUT, X2_OUT,
X3_OUT, X4_OUT, [5], X6_OUT]).
1 root@794bb5fbd58a:~/DCC888# gprolog 2 GNU Prolog 1.3.0 3 By Daniel Diaz 4 Copyright (C) 1999-2007 Daniel Diaz 5 | ?- [rd1]. 6 compiling /home/ronghua.zhou/DCC888/rd1.pl for byte code... 7 /home/ronghua.zhou/DCC888/rd1.pl compiled, 21 lines read - 4036 bytes written, 6 ms 8 9 yes 10 | ?- X5_OUT = [5], 11 X1_IN = [], 12 X2_IN = [3], 13 X3_IN = [3], 14 X4_IN = [5], 15 X5_IN = [5], 16 X6_IN = [3, 5], 17 OUT = [], 18 X2X1_OUT = [], 19 _OUTX2_OUT = [3], 20 X3_OUT = [3], 21 X4_OUT = [5], 22 X6_OUT = [6] . 23 24 X1_IN = [] 25 X1_OUT = [] 26 X2_IN = [3] 27 X2_OUT = [3] 28 X3_IN = [3] 29 X3_OUT = [3] 30 X4_IN = [5] 31 X4_OUT = [5] 32 X5_IN = [5] 33 X5_OUT = [5] 34 X6_IN = [3,5] 35 X6_OUT = [6] 36 37 yes 38 | ?- length(X5_OUT, 1), solution([X1_IN, X2_IN, X3_IN, X4_IN, X5_IN, X6_IN, 39 X1_OUT, X2_OUT, X3_OUT, X4_OUT, X5_OUT, X6_OUT]). 40 41 X1_IN = [] 42 X1_OUT = [] 43 X2_IN = [3] 44 X2_OUT = [3] 45 X3_IN = [3] 46 X3_OUT = [3] 47 X4_IN = [5] 48 X4_OUT = [5] 49 X5_IN = [5] 50 X5_OUT = [5] 51 X6_IN = [3,5] 52 X6_OUT = [6] 53 54 yes 55 | ?- solution([X1_IN, X2_IN, X3_IN, X4_IN, X5_IN, X6_IN, 56 X1_OUT, X2_OUT, X3_OUT, X4_OUT, X5_OUT, X6_OUT]). 57 58 Fatal Error: local stack overflow (size: 8192 Kb, environment variable used: LOCALSZ)
4.2 解決迴圈worklist的方法
混沌迭代:假定很多約束放在一個袋子裡面,每次從袋子中提取一個約束,並解析它,直到所有約束都從袋子裡面取出。
4.2.1 用混沌迭代解決reaching definition
1 for each i ∈ {1, 2, 3, 4, 5, 6} 2 IN[xi] = {} 3 OUT[xi] = {} 4 repeat 5 for each i ∈ {1, 2, 3, 4, 5, 6} 6 IN'[xi] = IN[xi]; 7 OUT'[xi] = OUT[xi]; 8 OUT[xi] = def(xi) ∪ (IN[xi] \ kill(xi)); 9 IN[xi] = ∪ OUT[s], s ∈ pred[xi]; 10 until ∀ i ∈ {1, 2, 3, 4, 5, 6} 11 IN'[xi] = IN[xi] and OUT'[xi] = OUT[xi]
4.2.2 抽象之後的混沌迭代演算法
1 x1 = ⊥ , x2 = ⊥ , … , xn = ⊥ 2 do 3 t1 = x1 ; … ; tn = xn 4 x1 = F1 (x1, …, xn) 5 … 6 xn = Fn (x1, …, xn) 7 while (x1 ≠ t1 or … or xn ≠ tn)
根據不動點原理(Fixed Point Theory,參見Fixed Point Theory - an overview | ScienceDirect Topics),可以證明該計算過程存在一個不動點,也就是即使在不規定具體執行順序的情況下,也能正常終結。但這個原始演算法的複雜度是O(n5),幾乎比所有真實存在的演算法的複雜度都要高。但如果給定條件是所有節點之間是有序的,這個複雜度可以最終降維到O(n),也就是線性複雜度,很多實驗也證實了這種假定。
下面是課程中給出混沌迭代演算法是線性複雜度的證據,列舉了汙染分析和指標分析的複雜度相對程式規模的增長情況(但我有點疑惑,左邊座標系是指數座標系,如果複雜度相對指數座標系是線性,是不是表示這個複雜度是指數複雜度?):
4.2.3 混沌迭代計算的加速
對等式的計算如果不加排序,複雜度是驚人的,但通過一定的計算排序,可以有效降低計算複雜度。
找到約束變數之間的依賴圖:
4.2.4 混沌迭代的worklist表達
1 x1 = ⊥ , x2 = ⊥ , … , xn = ⊥ 2 w = [v1, …, vn] 3 while (w ≠ []) 4 vi = extract(w) 5 y = Fi (x1, …, xn) 6 if y ≠ xi 7 for v ∈ dep(vi) 8 w = insert(w, v) 9 xi = y
為了方便講解,原來的計算方法對6個程式點,存在12個引數(6個IN,6個OUT),但實際上IN和OUT能相互推導,所以只保留6個輸入的集合不會影響計算效果,簡化版的reaching definition的推導程式如下:
1 solution([X1_IN, X2_IN, X3_IN, X4_IN, X5_IN, X6_IN]) :- 2 X1_IN = [], /* F1 */ 3 diff(X3_IN, [3, 5, 6], XA), union(XA, X1_IN, XB), union(XB, [3], X2_IN), /* F2 */ 4 X3_IN = X2_IN, /* F3 */ 5 diff(X5_IN, [3, 5, 6], XC), union(XC, X1_IN, XD), union(XD, [5], X4_IN), /* F4 */ 6 X5_IN = X4_IN, /* F5 */ 7 union(X2_IN, X5_IN, X6_IN), !. /* F6 */
注意,上面solution函式中的沒一行都是生成worklist演算法中y的Fi,worklist中的dep(vi)是之前依賴圖中的依賴關係集合,例如對程式點1,實際上所有程式點的輸入和輸出集合都依賴它,即使簡化之後只剩下輸入集合,也是2到6的輸入集合都依賴程式點1,但dep(vi)只計算直接依賴,也就是說dep(v1)={x2, x4},為了便於推導,我們先生成一個簡化版依賴圖。
1 digraph { 2 "X1_IN" -> {"X2_IN" "X4_IN"} 3 "X2_IN" -> "X3_IN" -> "X2_IN" 4 "X4_IN" -> "X5_IN" -> "X4_IN" 5 {"X2_IN" "X4_IN"} -> "X6_IN" 6 }
大家有沒有從這幅簡化版的依賴圖裡面看到和CFG之間的關聯關係?是的,簡化版的依賴圖基本上就是CFG(如果對某個BB不只是一條指令的情況下,簡化依賴圖裡面的每個變數會變成一個變數的集合,但依賴關係還是和CFG保持一致)。所以後面我們做worklist的計算表的時候,只需要看著CFG就可以做出來了。
現在讓我們手工推導一下worklist表,注意推導過程中的insert和extract使用的LIFO棧(Last-In, First-Out, 後進先出 ),而且我們不關心棧裡面是否已經有對應的元素:
4.2.5 尋找更好的遍歷順序
因為前面畫出來的依賴圖是對部分節點並不存在迴圈,所以它不存在一個拓撲意義上的遍歷順序,但我們可以有一個準遍歷順序(quasi-ordering)。
深度優先搜尋(Depth-First Search),也稱為深度優先遍歷(Depth-First Span),從根節點開始遍歷。將當前遍歷節點加入遍歷過的列表,並對與當前節點有聯通邊的所有節點進行深度優先遍歷。下面是python的虛擬碼:
1 def dfs(graph, root, visitor): 2 """DFS over a graph. 3 Start with node 'root', calling 'visitor' for every visited node. 4 """ 5 visited = set() 6 def dfs_walk(node): 7 visited.add(node) 8 visitor(node) 9 for succ in graph.successors(node): 10 if not succ in visited: 11 dfs_walk(succ) 12 dfs_walk(root)
圖中的DFS和普通的樹不一樣,因為可能一個節點有多個父節點,可能會存在部分節點先遍歷子結點再遍歷父節點的情況。例如對之前那個例程的CFG,DFS的順序是[1, 2, 3, 6, 4, 5],畫成圖這樣的:
後根序列(post-order):和DFS的遍歷過程類似,但注意這裡是先遍歷,再加入到列表。生成的順序是[3, 6, 2, 5, 4, 1](按演算法本身來說也可能生成[6, 3, 2, 5, 4, 1])。下面是後根序列的python程式碼:
1 def postorder(graph, root): 2 """Return a post-order ordering of nodes in the graph.""" 3 visited = set() 4 order = [] 5 def dfs_walk(node): 6 visited.add(node) 7 for succ in graph.successors(node): 8 if not succ in visited: 9 dfs_walk(succ) 10 order.append(node) 11 dfs_walk(root) 12 return order
畫出來的圖是這樣的:
反向後根遍歷(Reverse postorder):顧名思義,就是把後根遍歷的序列轉置以下,是後根遍歷的逆序,簡稱rPostorder。生成的順序是[1,4, 5, 2, 6, 3]。圖和後根遍歷的一樣,只不過方向相反。
當然,一個圖轉換成DFS或者rPostorder的時候,由於對多個節點共父節點的情況下,這多個節點之間的順序是不確定的,所以可能會存在多個DFS或者Postorder,也就會有多個rPostorder。
基於rPostorder的worklist演算法虛擬碼:
1 insert(v, P): 2 return P ∪ {v} 3 4 extract(C, P): 5 if C = [] 6 C = sort_rPostorder(P) 7 P = {} 8 return (head(C), (tail(C), P)) 9 10 main: 11 x1 = ⊥ , x2 = ⊥ , … , xn = ⊥ 12 C=[], P={v1, ... , vn} 13 while (C ≠ [] || P ≠ {}) 14 vi, C, P = extract(C, P) 15 y = Fi (x1, …, xn) 16 if y ≠ xi 17 for v ∈ dep(vi) 18 P = insert(v, P) 19 xi = y
下面的rPostorder版本的worklist演算法是按[1, 2, 3, 4, 6, 5]的排序來進行的,換成其他rPostorfer的序列推匯出來的步數是一樣的。
這樣排序造成的一個直接後果就是所有子結點的遍歷必須在父節點之後,這樣確保它的所有前驅節點都先遍歷,這樣導致第一輪C計算到[]之後,第二輪遍歷過程中P不會再有新的元素出現,確保兩輪遍歷之後演算法結束。
從上圖看,第一輪遍歷完之後,所有輸入集合基本上就不變了,那第二輪遍歷是否可以省略?
4.3 強子圖
強子圖(Strong Components,簡稱SC或者SCG):如果某個圖中存在一個最大的子圖,子圖中任意節點之間都聯通,則稱為強子圖,也可以叫強連通子圖(簡稱SCC或者SCCG)。
由於SC的拓撲一致性,也就是說把SC當做一個普通節點計算出來的約束系統和把SC中的所有節點拆分開之後計算出來的約束系統是一致的,所以通常可以用SC來對圖進行降維。
降維之後的CFG的dot描述如下:
1 digraph { 2 "1: b1" -> {"2: b2, 3: x=a1" "4: b3, 5: x=a2"} -> "6: x=a3" 3 }
畫出來的圖是這樣的:
基於SCC的拓撲一致性,可以先將某個SCC或者其前驅程式點的約束計算完(計算完的意思是計算到某個不動點),再計算該SCC後繼的程式點,計算後繼節點的程式點時不需要往前遍歷。基於SCC的worklist遍歷過程:
4.4 演算法簡化:輪轉迭代
輪轉迭代不用儲存一個待解析的列表,也不用怎麼實現extract和insert,但會迭代更多次數。
1 x1 = ⊥ , x2 = ⊥ , … , xn = ⊥ 2 change = true 3 while (change) 4 change = false 5 for i = 1 to n do 6 y = Fi (x1, …, xn) 7 if y ≠ xi 8 change = true 9 xi = y
15步可以把約束系統計算完畢:
4.5 集合的表達
4.5.1 bit-vectors點陣圖矩陣
點陣圖矩陣一般在緊密分析中適用,每個元素佔用一個bit,空間上只需要N/K個字,其中N是元素個數,K是每個字的bit位個數,插入的複雜度是O(1),執行復雜度是線性的。
4.5.2 其他表達方式
雜湊表和連結串列通常對稀疏分析時適用,因為大多數元素並不會在某個程式點有值,適用雜湊表可以只儲存有值的元素。
4.6 worklist演算法簡史
第一次引入worklist:Kildall, G. "A Unified Approach to Global Program Optimization", POPL, 194-206 (1973)
第一次使用SCC進行資料流分析:Horwitz, S. Demers, A. and Teitelbaum, T. "An efficient general iterative algorithm for dataflow analysis", Acta Informatica, 24, 679-694 (1987)
其他分析:Hecht, M. S. "Flow Analysis of Computer Programs", North Holland, (1977)