LLVM笔记(5) - SMS
1. SMS介紹
? SMS(Swing Modulo Scheduling, 搖擺模調度)是一個基于循環的與架構無關的SWP(software pipelining)指令調度框架, 其目的是通過將當前迭代的指令與上一迭代同時發射來提升并行度. 考慮以下偽代碼:
? 不考慮跳轉指令, 該循環需要獨立發射三條指令(指令之間相互依賴). 但如果將其改變為以下方式:
? 指令1與指令3間無直接依賴, 因此可以將本次迭代的指令3與下次迭代的指令1放在同一時刻發射, 提高并行度.
? 關于SMS原理的更多了解, 參見include/llvm/CodeGen/MachinePipeliner.h中涉及的三篇論文.
? 1. "Swing Modulo Scheduling: A Lifetime-Sensitive Approach"
? 2."Lifetime-Sensitive Modulo Scheduling in a Production Environment"
? 3. "An Implementation of Swing Modulo Scheduling With Extensions for Superblocks"
2. SMS實現分析
? SMS算法由三部分組成. 首先, 計算出最小初始間隔(MII, minimal initiation interval). 第二步建立dependence graph, 計算每條指令的相關信息(ASAP ALAP MOV Height Depth ...). 最后使用論文中提及的方式為節點排序.
3. 常見概念
? MII(minimal initiation interval)指完成循環所需的最小間隔, 它等于ResMII與RecMII兩者的較大值, MII是理想的調度結果. ResMII(Resource MII)是根據一次循環所需的功能單元(FU, function unit)去除以機器所有功能單元的結果, 即如果一個循環包含4條指令而芯片只有兩個運算單元則(不考慮指令間依賴)最少需要2 cycle才能完成一次循環. 更進一步, 如果循環中包含多類FU的使用(且每類FU之間相互無法替換), 則ResMII是分別對每一類FU計算ResMII后的最大值. RecMII(Recurrence MII)是循環中環路完成一次迭代所需的最小間隔, 如果循環存在多條數據鏈路則分別計算后取其最大值.
? Prolog / Epilog分別指實現SWP后被提前到循環之前/放到循環之后執行的代碼塊. Kernel指實現SWP后的循環代碼塊.
? Dependence Graph指指令間存在的依賴關系, 具體分為對寄存器的依賴與對內存的依賴. 對寄存器的依賴又分三種: 先寫后讀(RAW, read after write, 數據依賴, 又稱真依賴)在LLVM中用SDep::Data表示, 先讀后寫(WAR, write after read, 反依賴)在LLVM中用SDep::Anti表示, 先寫后寫(WAW, write after write, 輸出依賴)在LLVM中用SDep::Output表示. 對同一地址的讀寫也會產生依賴, 在LLVM中統一使用SDep::Order表示(不論是laod-store還是store-load還是store-store), 另外特殊指令帶有side-effect屬性時也使用順序依賴表示. 順序依賴又分為Barrier, AliasMem, Artificial, Weak等多個子類型, 其具體定義見include/llvm/CodeGen/ScheduleDAG.h.
4. 代碼分析
? MachinePipeliner類的入口為MachinePipeliner::swingModuloScheduler(), 函數首先檢查循環是否僅包含一個BB(即沒有change of flow). SwingSchedulerDAG是ScheduleDAGInstrs的子類, 父類的startBlock()/enterRegion()/exitRegion()/finishBlock()都是virtual的, 用于特定調度器的initial/clean up. SwingSchedulerDAG::schedule()(defined in lib/CodeGen/MachinePipeliner.cpp)實現swing modulo調度算法.
? 4.1. 建立dependence graph
? 為建立依賴圖首先需要獲取alias分析結果(否則無法分析order dependence), 再調用公共框架接口ScheduleDAGInstrs::buildSchedGraph()(defined in lib/CodeGen/ScheduleDAGInstrs.cpp)建立依賴圖(關于ScheduleDAGInstrs類后文分析). 注意公共框架返回的依賴圖是基于順序執行的代碼塊的, 由于SWP的特殊性我們還要加上跨迭代的依賴(loop carried dependence, 即本次迭代的指令與下次迭代間的依賴).
? 由于在SSA模式下跨迭代的寄存器使用必然存在phi node, 因此我們只需為phi node添加依賴. SwingSchedulerDAG::updatePhiDependences()(defined in lib/CodeGen/MachinePipeliner.cpp)會為phi node添加data dep如果存在指令引用該phi, 并為該指令添加anti dep(在本次迭代的引用指令與下次迭代的phi指令間存在先讀后寫的關系).
? 對于跨迭代的內存訪問間依賴交給SwingSchedulerDAG::addLoopCarriedDependences()(defined in lib/CodeGen/MachinePipeliner.cpp)處理. 該接口會遍歷循環內所有load/store指令, 對每條load指令如果存在由alias關系的store則檢查是否能通過load指令reach到store指令(一般是順序load后store的情況, 不需要額外dep), 如果不能reach則為該sotre添加barrier dep. 再調用ScheduleDAGTopologicalSort::InitDAGTopologicalSorting()(defined in lib/CodeGen/ScheduleDAG.cpp)為節點排序方便之后處理, 至此依賴圖基本建立完畢.
? 當前框架下還有兩個針對依賴圖的優化, SwingSchedulerDAG::changeDependences()會嘗試轉換指令使用前次迭代的值來降低RecMII(主要針對pre-inc/post-inc的load/store, 縮短dep chain). SwingSchedulerDAG::postprocessDAG()用來調用一些基于特定目的實現的Mutation的hook, 當前框架實現一個名為CopyToPhiMutation, 這個沒看懂干嘛的, 為什么延后COPY的調度可以獲得更好的性能?
? 為更具象的描述代碼, 這里以一個testcase為例:
? 其中__restrict是c99引入的關鍵字, 在這里用以減少內存訪問依賴, #pragma nounroll是llvm的pragma, 保證循環不被展開優化, 用在這里減少循環內指令數(變相減少打印), 也可以去掉以上語法糖來觀察SMS的變化. 篇幅有限, 這里截取部分打印.
? loop block before SMS
? dependence graph
? NodeFunction & NodeSets
? Schedule
? 先來看下dependence graph, 循環內總共四條Machine IR, 其中兩條為PHI, 另外兩條分別為load與store. 先看第一條PHI, 因為它的def-reg %2被store指令使用(先寫后讀)所以存在真依賴, 同理它的use-reg %5被store指令定義所以又存在反依賴. 另一條PHI與load指令間依賴關系類似. 最后load定義的%15被store使用, 所以兩者間還有一個真依賴. 注意雖然PHI與store之間都存在反依賴, 但其含義不同: PHI指令的反依賴表示PHI不能調度到本迭代的store后, store的反依賴表示不能調度到下次迭代的PHI之后.
? 4.2. 計算ResMII/RecMII
? ResMII比較容易計算(最簡單的辦法是指令數除以功能單元數, 當然代碼中使用更復雜的DFA做更精確的評估), SwingSchedulerDAG::calculateResMII()基于DFA實現了ResMII的計算.
? RecMII的計算則復雜許多, 首先我們要找到所有的關鍵路徑(critical path). SwingSchedulerDAG::findCircuits()會首先查找循環中所有的環路并將其記錄到NodeSets中. 思路是首先交換反依賴(否則跨迭代環路不結束), 然后構建一個依賴矩陣, 依賴矩陣的作用是記錄所有backedge, 判斷回邊的依據: 到phi的反依賴(必然是回邊), 順序的輸出依賴鏈上第一個與最后一個(最后一個不能調度到下個迭代的第一個之前), 以及讀寫之間的順序依賴(為什么寫寫之間的順序依賴不算回邊? 我的猜測是如果循環中只有寫寫那么相對順序就不重要了, 只要保證最后一次寫正確即可?). 最后通過索引改矩陣查找從每個節點出發回到該節點為止的一條鏈路.
? 可以看到例子中一共找到兩條環路, 第一條phi到store指令的基址, store指令基址再到phi, 第二條類似phi到load指令再到phi. 找到所有環路后即可計算RecMII, 對每條環路計算所需的latency總和取最大值即可.
? 4.3. 計算NodeFunction與NodeOrder
? 為了方便之后的調度還需要計算節點的一些特殊屬性, SwingSchedulerDAG::computeNodeFunctions()代碼注釋已經很好的說明這些屬性的作用. 如字面意思ASAP指該節點最早調度時機, ALAP值該節點最晚調度時機, MOV指該節點可調度空間(等于ALAP - ASAP), D指節點深度, H指節點高度. 注意由于有zero latency指令的存在(一般是各類phi copy reg_sequence等偽指令), ASAP/ALAP與D/H存在區別.
? 計算NodeOrder(TODO).
? 4.4. 調度
? SwingSchedulerDAG::schedulePipeline()按NodeOrder順序排序. 節點所插入的cycle是由NodeFunction與已排序的節點間的依賴決定的. SMSchedule::computeStart()用來計算當前節點最早與最晚插入時機, 如果計算的earlystart > latestart則調度失敗, 一般情況是NodeOrder順序非最優順序. 否則調用SMSchedule::insert()嘗試插入節點(注意偽指令一般視作zerocost指令無需排序), 由于NodeOrder過程中只考慮了順序的依賴, 因此這里還要考慮跨迭代的依賴(即不光考慮當前cycle是否能插入該節點, 還要考慮(cycle + II * stage)中的節點是否與該節點沖突, 否則最后無法將later stage中指令折疊到當前cycle). 所有節點排序完成后就得到順序依賴的指令調度, 此時調用SMSchedule::finalizeSchedule()將later stage指令往前折疊. 該接口分為三步, 第一將later stage指令拷貝到first stage, 第二步建立每個stage的寄存器映射, 最后為每個cycle內的指令重新排序(包括zerocost指令). 第三步通過SMSchedule::orderDependence()實現.
? 4.5. 生成pipelined loop
? finalizeSchedule()產生了并行的循環代碼, 但這還不是最終的kernel, 期間還需要完成三件事: 重新計算寄存器(對應不同迭代), 重新生成phi節點, 生成prolog與epilog. 這些任務由SwingSchedulerDAG::generatePipelinedLoop()完成. 主要接口updateInstruction()根據VRMap與stage為寄存器重命名, generateExistingPhis()替換之前的phi節點, generatePhis生成新的phi節點. 這塊是詬病最多的代碼, 像reduceLoopCount()這些為Hexagon實現的hack, 最近社區還有討論重構這塊代碼(http://lists.llvm.org/pipermail/llvm-dev/2019-July/134002.html).
5. 優化&問題定位
? SMS是比較復雜的后端優化模塊, 問題定位主要抓以上幾個流程點: 首先看依賴圖是否有問題, MII的計算是否正確, NodeFunction與NodeOrder計算, 排序結果是否有問題, prolog/epilog是否有誤. 每個流程的問題不能影響下一流程. e.g. SMS流程后出現use before def問題, 如果覺得排序有誤就不用看寄存器映射是否有問題, 先分析排序是否正確, 因為finalizeSchedule()后指令就不能重排序, 而VRMap是依據當前指令排序生成的, 排序不正確后面修改phi也不能解決問題.
轉載于:https://www.cnblogs.com/Five100Miles/p/11223493.html
總結
以上是生活随笔為你收集整理的LLVM笔记(5) - SMS的全部內容,希望文章能夠幫你解決所遇到的問題。