編譯過程中的並行性優化概述

編譯過程中的並行性優化概述

浙江大學《編譯原理》課程報告

目錄:


我的GIS/CS學習筆記:https://github.com/yunwei37/myClassNotes
<一個浙大GIS/CS小白的課程學習筆記 >


前言

並行性是指計算機系統具有可以同時進行運算或操作的特性,在同一時間完成兩種或兩種以上工作。並行性等級可以分爲作業級或程序級任務級或程序級指令之間級指令內部級

指令級並行 (Instruction Level Parallelism, ILP)是指爲了實現多個操作的並行執行而在處理器和編譯器的設計中採用的一系列技術。指令級並行是現代高性能處理器的重要特徵。對於在一個具有指令級並行機制的處理器上程序的並行能力,需要考慮以下因素:

  • 程序中潛在的並行性,或者說程序中預算之間的依賴關係;例如具有簡單的控制結構和規則的數據訪問模式的數值應用中的並行性就相對較多;
  • 處理器上可用的並行性,比如可以用以計算的硬件資源的數目;
  • 從原來的順序程序中抽取並行性的能力;
  • 在給定的指令調度約束下找到最好的並行調度方案的能力;

並行性抽取和並行執行的調度可以通過軟件靜態完成,也可以通過硬件動態完成,或二者互相結合。編譯中主要涉及的就是軟件相關的靜態過程,即如何通過在編譯的過程中進行指令抽取指令調度,來達到更好的並行性和運行速度。傳統優化編譯器的後端主要包括寄存器分配,求值順序確定以及指令選擇等幾部分內容;而對於需要支持指令級並行編譯的後端來說,核心的問題是指令
調度。指令調度決定操作執行的相對順序,各操作的具體執行時間及使用哪些硬件資源等。

本文希望從並行性相關的處理器體系結構實現、基本塊調度算法、全局調度算法,以及軟件流水線化等方面來介紹編譯過程中的並行性問題。


並行相關的處理器體系結構

並行性的基礎是現代高性能處理器的硬件能夠在一個時鐘週期能執行多條指令。現流行的並行技術大都可以從三個方面實現:資源重複、資源共享、時間重疊。其中主要應用到的技術如下:

  • 流水線技術

    計算機中的流水線技術是把一個重複的過程分解爲若干個子過程,每個子過程與其他子過程並行進行。從本質上講,流水線技術是一種時間並行技術。通常我們描述的指令級並行性指的是在一個時鐘週期內能發射多條指令,但如果使用流水線技術,由於一個指令需要多個時鐘週期完成,因此仍然存在指令級並行的情況:每個時鐘週期都可以取得一個新指令,而前面的指令還在流水線中執行,例如一個簡單的五級流水線,可以分爲取址->譯碼->執行->訪存->寫回五個階段進行,其時空圖如下:

在這裏插入圖片描述

如果後續指令所需要的結果在此時已經可用,那麼流水線就可以流出一條指令。但對於部分存在數據相關或者分支跳轉的指令而言,下一條指令所需要的內容依賴於上一條指令的執行結果,此時就需要進行一定的調度或數據傳輸來避免流水線停頓。
  • 多指令發送

    流水線技術雖然已經利用了一定的並行性來加速程序執行,但如果能通過配置多個可用的功能部件在每個週期發送多條指令,並行性還可繼續提升,即多指令發送技術,也稱多發射技術。常見的多發送機器有通過軟件管理其併發性的 VLIW (Very Long Instruction Word,超長指令字) ,即通過一種非常長的指令組合,把許多條指令連在一起增加運算速度;或通過硬件管理的 超標量 機器。

    簡單的硬件指令調度器根據指令獲取的順序執行指令,如果其碰到依賴先前指令的指令,需要等待依賴關係的解除(計算結果可用)才能進行下一步的計算。更加複雜的指令調度器可以通過動態地調整指令執行的序列來避免相關性造成的阻塞。

  • 多核處理器

    近年來,由於摩爾定律的限制,僅僅提高單核芯片的速度會產生過多熱量且無法帶來相應的性能改善,因此引入了多核芯片來增加並行性。多核處理器是指在一枚處理器中集成兩個或多個完整的計算引擎(內核),此時處理器能支持系統總線上的多個處理器,由總線控制器提供所有總線控制信號和命令信號。多核處理器對應於線程級並行性。本文主要涉及指令級並行,因此不做過多的介紹。

  • SIMD

    SIMD 擴展部件是又一個現代計算機在硬件方面對並行性的支持技術,它是集成到通用處理器中的加速部件, 旨在發掘多媒體程序和科學計算程序的數據級並行。SIMD 擴展部件能夠對多媒體程序中的數據進行並行處理,提升了多媒體程序的運行速度;在特定的微處理器體系結構上,SIMD 擴展指令允許將原來需要多次裝載的內存中地址連續的數據一次性裝載到向量寄存器中,通過一條 SIMD 擴展指令實現對 SIMD 向量寄存器中所有數據元素的並行處理,如intel的MMX,SSE,AVX等指令集。


代碼調度的相關約束

在討論代碼調度的相關算法之前,我們首先需要看一下代碼調度所需要遵守的一些基本約束條件。約束可以大致分爲三種類型:

  • 控制依賴約束:所有在源程序中執行的操作都必須在優化的程序中執行;
  • 數據依賴約束:優化後的程序中的操作必須和源程序中的相應操作生成相同結果;
  • 資源約束:特定機器上的資源是有限的,不能超額使用。

這些約束保證程序的優化可以正常進行,並生成和源程序相同的結果。但由於代碼調度改變了指令執行的順序,有可能優化後的程序在執行某一點上的內存狀態與優化前任何一點都不匹配。

我們來看看具體的一些依賴問題。

數據依賴

簡單來說,如果兩個操作訪問同一個變量,且這兩個操作中有一個爲寫操作,此時這兩個操作之間就存在數據依賴性,並且它們之間的相對執行順序必須保持不變。在代碼調度中可能出現的數據依賴有:

  1. 真依賴:即寫之後再讀;
  2. 反依賴:讀之後再寫,如果調度時寫操作在讀操作前發生,就可能讀到錯誤的值。
  3. 輸出依賴:寫之後再寫,如果順序調換則會導致被寫位置上存放的是錯誤的值。

其中,後兩者被稱爲存儲相關的依賴,可以通過在不同的內存位置存放不同的值來消除這些依賴關係。

內存訪問依賴

如果兩個不同的內存訪問指向同一個位置,就有可能存在內存訪問之間的依賴關係。內存訪問依賴關係比較複雜,尤其是對於非類型安全的語言(如C語言),要證明任意一對基於指針的內存訪問之間的獨立性需要負債的分析過程。主要的分析可以有以下幾種:

  • 數組的數據依賴分析:區分數組元素訪問中的下標值;
  • 指針別名分析:如果兩個指針指向同一個對象,即互爲別名;
  • 過程間分析:關於全局變量與參數之間的問題。

寄存器使用與並行性的折衷

在並行分析和調度中的機器無關中間表示所使用的無限多個僞寄存器必須被映射到目標機器上的有限寄存器;而把幾個僞寄存器映射到同一個物理寄存器會生成一定的存儲依賴,導致限制了指令級的並行性。從另一方面來說,並行性也產生了更多的存儲需求,從而增大了寄存器分配的壓力。因此,儘量降低寄存器使用數量的目標與最大化指令並行性的目標直接衝突。

寄存器分配階段與代碼調度階段的順序也會影響到並行性與存儲器數量。因此,在某些時候可以採用層次化的方式來處理,例如從最內層循環開始進行代碼優化,先進行指令調度,再進行寄存器分配,再對代碼進行調度;對外層循環依次重複此過程。

控制依賴

如果說指令A的結果決定了指令B是否執行,那麼就可以說指令B是控制依賴於指令A的。一個優化後的程序必須執行源程序中所有的運算,也可以執行更多的指令來增加並行性。

投機執行

如果我們知道一條指令可能會執行,並且有空閒的資源來"免費"執行這個指令,就可以先投機地執行這個指令;如果這個投機是正確的,就能加速程序執行。如內存加載指令就能從中獲取較大好處,很多現代高性能處理器都有對其的支持功能,如:

  • 預取指令
  • 毒藥位
  • 帶斷言的執行

基本塊調度算法

基本塊是連續三地址狀態的最大序列,其中控制流只能在塊的第一個語句中輸入,並在最後一個語句中停留,而不會停止或分支。

對於一個由機器指令組成的基本塊中的指令進行調度以獲取最優解,這個時間複雜度是NP完全的。但在實踐中,由於基本塊之間的高度約束的運算較少,因此用簡單的調度算法是可行的。這裏介紹一個列表調度的算法。

數據依賴圖

數據依賴圖是調度算法中用到的一個重要工具。我們可以把每個由機器指令組成的基本塊標識成爲一個數據依賴圖(data-dependence graph), G = (N,E),其中節點集合N表示基本塊中機器指令的運算,而有向邊集合E表示運算之間的數據依賴約束。G的節點集合和邊及可以按照如下方式構造:

  1. 在N中的每個運算n爲一個節點,有個資源預約表RTn,其值就是n的運算類型所對應的資源預約表;
  2. E中的每條邊e有一個表示延時的標號de,表明目標節點必須在源節點發出後至少de個時鐘週期之後發出。

數據依賴圖的實例如下:

在這裏插入圖片描述

列表調度算法

從數據依賴圖和資源預約表就能清晰地看到指令之間的依賴關係,因此,我們可以採用簡單的方法,即使用帶優先級的拓撲排序訪問數據依賴圖的各個節點,就能得到基本塊調度的順序。換句話說,算法根據數據依賴圖中每個節點和之前已調度的節點之間的數據依賴約束,計算出能執行該節點的最早時間位置。

  • 輸入:一個機器資源向量 R = [ r1, r2 ... ], 其中ri是第i種資源的可用單元數目;以及一個數據依賴圖 G = (N,E)
  • 輸出:一個調度方案S, 將N中的每個運算映射到時間位置中。

算法僞代碼:

在這裏插入圖片描述

列表調度算法不進行回溯,對每個節點只進行一次指令調度,並使用一個啓發式的優先級函數函數從已就緒的節點中選擇下一個調度的節點。它具有如下性質:

  • 在不考慮資源約束的情況下,最短的調度方案根據關鍵路徑給出;
  • 如果運算都是獨立的,調度方案的長度受到可用資源的約束;
  • 可以使用源代碼中的順序決定運算之間難分先後的情況;

全局代碼調度

爲了更好地利用機器資源,我們還可以考慮將一些指令從一個基本塊移動到另一個基本塊的代碼調度,這種策略就稱爲全局調度。全局指令調度與寄存器分配可以說是指令級並行編譯中的核心問題。爲了正確地進行全局調度,除了數據依賴關係以外,我們也要考慮控制依賴關係。

我們需要保證以下兩點才能進行調度:

  1. 所有在源程序中執行的指令都會在優化後的程序中運行;
  2. 額外投機執行的指令不能產生任何副作用;

基本代碼移動

局部與全局代碼調動的例子:

在這裏插入圖片描述

就像上述調度,在全局代碼移動過程中,我們可以沿着一個執行路徑上下移動指令。可以根據基本塊之間的支配關係考慮指令移動的方式:

  • 如果每個從控制流圖入口處到達基本塊B1的路徑都經過一個基本塊B2,那麼就認爲B2支配B1;
  • 如果從基本塊B1到達控制流圖出口處的路徑都經過B2,那麼就認爲B2反向支配B1;
  • 如果B1支配B2且B2反向支配B1,則二者控制等價

在一條路徑上的一堆基本塊之間可能支配關係和反向支配關係都不具有。

對於可能的全局代碼移動方式,可以總結如下:

  • 在控制等價的基本塊之間移動指令最簡單且性價比最高;
  • 在沿着控制流路徑向上(向下)的代碼移動中,如果源基本塊不反向支配(支配)目標基本塊,可能需要執行額外的運算;
  • 在沿着控制流路徑向上(向下)的代碼移動中,如果目標基本塊不支配(反向支配)源基本塊,就可能需要補償一些相應的代碼;
  • 如果在沿着控制流路徑向上(向下)的代碼移動中,源和目的基本塊之中既不支配,也不反向支配,那麼可能既需要需要執行額外的運算又需要補償一些相應的代碼。

同時,代碼移動可能也會改變運算之間的數據依賴關係,因此每次代碼移動之後都必須更新它。

全局調動算法

  • 基於區域的調度算法:

    區域是一個控制流圖的子集,它只能ton過一個入口基本塊到達。對於一個簡單的全局調度器,可以採用基於區域的調度算法,它支持吧運算向上移動到控制等價的基本塊,或把運算向上移動一個分支,到一個支配前驅中:

    • 輸入:一個控制流圖和一個機器資源描述
    • 輸出:一個調度方案S
    • 僞代碼:

在這裏插入圖片描述

  • 循環展開:

    在代碼調度前少量地展開循環可以增加代碼移動的可能性,進而增加並行性,如下所示:

在這裏插入圖片描述

  • 相鄰壓縮:

    在基於區域的調度後可以再跟一個簡單的代碼處理過程,在這個過程中檢查各對相鄰的連續執行的基本塊是否有運算可以在他們之間上移或下移,以改進它們的執行時間。

動態調度

如果編程語言支持動態調度器,即可以根據運行時刻的情況產生新的調度方案,而不需要在運行之前對於所有的可能調度進行編碼,就能獲得更好的優化方案。


軟件流水線化

軟件流水線化也是一種重要的指令調度技術,就像硬件流水線的指令一樣,它通過並行執行來自不同循環體的指令來加快循環程序的執行速度, 在前一個循環體未結束前啓動下一個新的循環體,來達成循環體時間上的並行性。相比於簡單的展開循環(在提高性能的同時會導致代碼的膨脹),軟件流水線提供了一個方便的優化方法,能夠在優化資源使用的同時保持代碼的簡潔。

對於循環之間沒有數據依賴的 do-all 循環,我們可以用一個簡單的對比來說明軟件流水線同簡單循環展開的不同,下圖爲簡單的循環展開:

在這裏插入圖片描述

軟件流水線化通過將循環展開調度後中重複的部分進行循環,完成流水線。下圖爲軟件流水線化的結果:

在這裏插入圖片描述

在開始階段(1-6行)用來填充流水線的指令序列被稱爲序言;在穩定循環的部分(7-8行)被稱爲穩定狀態;用來排空流水線的指令序列(9-14行)稱爲尾聲

在軟件流水中,相鄰循環體的啓動時間間隔稱爲啓動間距。在軟件流水中再次應用循環展開,使同一時刻可以運行多個循環,可以使軟件流水實現分數值的啓動間距,同時基於展開的優化技術可以降低程序的資源需求和關鍵路徑的長度。但是,循環展開也會引起代碼量增長和寄存器需求增大,代碼量的增長會導致緩存的性能變差,寄存器需求的增大則有可能使軟件流水失敗。因此,軟件流水的核心問題之一就是展開因子的確定。

對於各個迭代之間的存在數據依賴關係的循環,也稱 do-access 循環,軟件流水線化也可以起到一定的效果:

在這裏插入圖片描述


SIMD

SIMD 擴展指令允許將原來需要多次裝載的內存中地址連續的數據一次性裝載到向量寄存器中,通過一條 SIMD 擴展指令實現對 SIMD 向量寄存器中所有數據元素的並行處理;這種執行方式非常適合於處理計算密集、數據相關性少的音視頻解碼等多媒體程序。

爲了高效利用SIMD擴展部件的特性,需要讓編譯器分析串行程序中控制流和數據流的特徵,識別程序中可以向量執行的部分,將標量語句自動轉換爲相應的SIMD 向量語句。SIMD 自動向量化編譯流程大致可分爲3部分,分別是發掘、優化和代碼生成:

在這裏插入圖片描述

  • 發掘:識別生成出 SIMD 指令,同時解決控制依賴對發掘的影響。SIMD 擴展部件可在不同的粒度進行識別向量化,包括面向基本塊內向量化、面向最內層循環或者循環嵌套的向量化以及面向函數級別的向量化。
  • 優化:首先減小輔助指令的開銷,同時考慮數據局部性、寄存器重用等工作。由於部分體系結構的 SIMD 指令只能從內存中存取連續對齊的數據,因此當程序中存在不對齊或不連續內存引用時需要通過移位或者重組等輔助指令才能組成向量。減少輔助指令的數量和提高輔助指令的效率,是增加程序 SIMD 向量化收益的關鍵問題。
  • 代碼生成:考慮平臺支持哪些數據類型和向量運算。直接面向特定平臺的 SIMD 向量化代碼生成存在許多不足,通常分階段並行編譯優化和虛擬向量是解決面向多平臺向量化的兩個方法。

當前 SIMD 編譯優化對數據對齊和重組研究很深入,面向循環和基本塊的向量化方法也很成熟。目前,針對SIMD 擴展部件編譯優化的主要關注熱點是生成高性能低功耗的代碼。


總結:

  • 體系結構相關問題
  • 數據依賴的定義與消除
  • 基本塊的數據依賴圖
  • 帶優先級的拓撲排序
  • 列表調度
  • 基本塊之間的代碼移動
  • 軟件流水線化
  • SIMD技術

參考資料

  • 《編譯原理》第二版,第十章、第十一章
  • 李文龍等,軟件流水中的循環展開優化,北京航空航天大學學報
  • 高偉等,SIMD 自動向量化編譯優化概述,軟件學報
  • 吳承勇等,協作式全局指令調度與寄存器分配,計算機學報
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章