基於高層次綜合器(Vivado HLS)的硬件優化[原創www.cnblogs.com/helesheng]

最近在寫一本Xilinx的FPGA方面的書,現將HLS部分內容在這裏分享給大家,希望大家喜歡,也歡迎批評指正。以下原創內容歡迎網友轉載,但請註明出處: https://www.cnblogs.com/helesheng

通過前面的學習,相信讀者已經基本掌握了高層次綜合器的基本使用方法,本小節將學習使用高層次綜合器提供的工具,優化上一小節實現的FIR濾波器消耗的FPGA資源和執行時間。

仔細閱讀代碼8.4,可以發現它每次只接收一個數據x(點),輸出一個數據(存儲在y所指向的存儲器中)。作爲一個有限衝擊響應濾波器(FIR),它的輸出只由當前輸入和以前的輸入計算產生,這段代碼將之前的輸入數據都緩衝在shift_reg數組之中。函數中的for循環兩個具體操作:其一,濾波器係數和輸入歷史數據之間的乘加運算。其二,對存儲在shift_reg數組中歷史數據進行移位,以將新的輸入存儲數組,並對老的歷史數據進行進一步老化。

可以看出上述代碼實現的FIR濾波器和用傳統高級語言經由CPU或DSP逐句實現的方法沒有多大區別,所有的計算仍然採用“串行”方式執行,並且需要在電路中增加大量流程控制邏輯。既沒有發揮可編程邏輯器件“並行”化執行算法的優勢,有沒有節約多少硬件資源,可以通過多種手段優化上述高層次綜合器代碼。

1、優化條件判斷語句

代碼8.4中for循環內部的if條件判斷語句將被高層次綜合器綜合後的硬件電路的工作效率十分低效,因爲判斷後兩個分支中的乘加和移位操作都只有在if判斷執行後才能真正執行,從而限制了後續優化中這些運算“並行化”的實現。是我們在優化中首先要解決的問題。另外,優化掉判斷語句還能省去綜合結果硬件電路中的條件判斷電路,從而降低電路的整體複雜度,可謂一舉多得。

仔細觀察代碼後可以發發現,去掉if條件判斷並不困難。因爲該判斷中的一個分支只發生在i == 0時,i等於其他值的情況都只會執行另外一個分支;另外,所有的分支在每次循環中都必將被依次執行。因此,可以將i爲0時對應的分支直接放到for循環的外部,最後執行即可,不會影響執行結果。得到優化代碼如下(注意,代碼中的Shift_Accum_Loop標籤和C語言中語句的標籤語法要素和作用完全相同,用於表示某些語句,不會產生任何實質性代碼或硬件)。 

 1 #define N 11
 2 typedef int coef_t;
 3 typedef int data_t;
 4 typedef int acc_t;
 5 void fir(data_t *y,data_t x)
 6 {
 7     coef_t C[N] = {53,0,-91,0,313,500,313,0,-91,0,53};
 8     static
 9     data_t shift_reg[N];
10     acc_t acc;
11     int i;
12     acc = 0;
13     Shift_Accum_Loop:
14     for(i = N-1;i > 0;i--){
15         shift_reg[i] = shift_reg[i-1];
16         acc += shift_reg[i] * C[i];
17     }
18     acc += x * C[0];
19     shift_reg[0] = x;
20     * y = acc;
21 }
代碼8.6

下圖是代碼8.6經高層次綜合器綜合後的結果,可以看到代碼的設計延時明顯降低,而乘加運算所使用的DSP48資源增加,顯然計算的並行度增加了。

圖8.3.1 優化後的綜合報告1 

2for循環拆分

如前所述,代碼8.6中的for循環中有兩種操作:乘加(標籤爲MAC)和移位(標籤爲TDL),將它們分別放在兩個for循環中,有利於高層次綜合器針對每個循環進行硬件優化,提高整體的代碼效率。拆分後的代碼如代碼8.7所示。

 1 #define N 11
 2 typedef int coef_t;
 3 typedef int data_t;
 4 typedef int acc_t;
 5 void fir(data_t *y,data_t x)
 6 {
 7       coef_t C[N] = {53,0,-91,0,313,500,313,0,-91,0,53};
 8       data_t shift_reg[N];
 9       acc_t acc;
10       int i;
11     TDL:
12       for(i = N - 1;i > 0;i--){
13         shift_reg[i] = shift_reg[i - 1];
14       }
15       shift_reg[0] = x;
16       acc = 0;
17     MAC:
18       for(i = N-1;i >= 0;i--)    {
19         acc += shift_reg[i] * C[i];
20       }
21       * y = acc;
22 }
代碼8.7

讀者綜合代碼8.7後會驚異地發現他的設計延遲相比代碼8.6不減反增!其實,這是很正常的現象,我們拆分for循環的目的是爲了針對乘加和移位分別進行優化,而現在還未進行任何具體優化操作,拆分循環只會增加循環控制電路的複雜程度,性能自然降低了。我們需要進一步分別優化MAC和TDL兩個循環。

3、移位循環的展開

如果不做專門的制定,高層次綜合器將把硬件電路配置爲順序執行for循環結構。但對於循環的第i次計算不需要上一次(i-1次)循環計算的執行結果的“非依賴”循環,如果在硬件電路中增加對該for循環的並行性支持,可以進一步充分發揮可編程邏輯器件的並行結構優勢,提升算法實現的效率。在代碼8.7中實現歷史數據存儲移位的TDL循環就屬於這類可以通過“循環展開”提升算法電路並行性的循環。

代碼8.8將TDL循環展開爲2次移位爲一組的新循環體,即每次循環中循環體執行兩個歷史數據移位操作,這需要綜合後得到的電路具有能夠同時執行兩個移位操作的硬件,但循環次數將降低爲原來的一半。

1 TDL:
2     for(i = N - 1;i > 1;i = i - 2){
3     shift_reg[i] = shift_reg[i - 1];
4     shift_reg[i - 1] = shift_reg[i - 2];
5     }
6     if(i == 1){
7     shift_reg[1] = shift_reg[0];
8     }
9     shift_reg[0] = x;
代碼8.8

代碼8.8由於每次對兩個數據進行了移位,TDL循環中對索引i的操作變爲每次減2(i == i - 2)。代碼8.8還在循環後面增加了一個if條件判斷,以防止循環次數爲奇數時最後一次移位未被執行,當然如果事先知道N的值是奇數還是偶數,也可以避免使用這條判斷語句。

上述展開操作,除了使用代碼8.8所示的“手工”代碼方式展開,還可以通過高層次綜合器支持的指令方式“自動”完成。在Vivado HLS開發工具中,添加指令的方式有兩種:

其一,直接在C語言源文件中嵌入編譯腳本指令,具體位置在需要配置的循環體for語句之後。指令格式爲:#pragma HLS unroll factor=2,以代表要求綜合器將循環並行化爲每次執行兩個移位操作。(注意,這種嵌入源碼的編譯指令都以#開始)

1 TDL:
2         for(i = N - 1;i > 0;i--){
3     #pragma HLS UNROLL factor=2
4             shift_reg[i] = shift_reg[i - 1];
5         }
6     shift_reg[0] = x;
代碼8.9

其二,通過圖形界面編輯腳本指令,並將所有編譯指令集中存放在專門的指令文件(Directive File)或循環所在的高級語言源文件中。這種方式需要在Vivado HLS界面右側的指令區(Directive)中找到需要添加編譯指令的循環,右擊該循環體進行配置(只有在打開C源文件的情況下,才能在右側的指令區找到需要編輯的循環)。指令編輯界面如下圖8.3.2所示。

 

 

圖8.3.2 高層次綜合器指令編輯界面

 在專門的指令文件添加過編譯指令的循環將會在Vivado HLS開發環境右側的指令區中看到以%開頭的編譯指令。如下圖8.3.3所示。

 

 圖8.3.3 添加了編譯指令的指令區

顯然,上述兩種方式添加編譯指令的展開方式具有閱讀、修改方便,不易出錯的優勢,要遠優於“手工”代碼展開方式。若比較代碼8.9所示的源碼添加指令的方式和圖8.2.14所示的在專門的指令文件中添加指令的方式哪種更好,則要根據開發者的需求來決定:源碼和指令的分離有利於在更高層面上對工程整體進行優化,而直接在源碼中寫入指令則有利於針對具體代碼的靈活優化。爲方便敘述,本書後續都採用直接在源碼中寫入指令的方式進行編譯配置和優化,但顯然所有指令也可以通過圖形界面完成配置。

最後,關於移位循環的展開還可針對移位寄存器本身的實現方式進行配置優化:若將移位寄存器shift_reg[N]配置在BRAM中,則由於BRAM只有兩個讀端口和一個寫端口,則在單個時鐘週期中最多完成一個寫和兩次讀,代碼8.8和8.9所希望的兩次移位操作就只能在兩個週期中才能完成。將所有的shift_reg[N]放在獨立的寄存器中可以實現在單個時鐘週期中完成多個移位寄存器單元讀寫的要求,使用編譯指令#progmaHLS array_parition variable=shift_reg complete能夠實現該功能。

4、乘加循環的展開

代碼8.7中的乘加循環(標籤爲MAC的循環)中的每次乘加操作需要讀取移位寄存器shift_reg[N]和係數寄存器C[N]中的值,並對它們相乘後累加到和acc中。如果要將這個乘加循環並行化,最大的障礙在於acc中的值存在“依賴關係”——即只有執行完上一次循環的加法得到acc的值後,才能執行下一次循環的加法。增加乘加循環並行度的關鍵是解除(或部分解除)這種依賴關係。

熟悉數字電路的讀者應該知道,可編程邏輯器件可以實現加數多於二個的多加數的加法器,例如我們使用可以對五個加數求和的加法器,就可以解除四次乘加運算之間的結果“依賴關係”,從而增加綜合結果電路的並行度。代碼8.10就是利用多加數加法器增加解決依賴關係的實例。

 1 MAC:
 2     for(i = N - 1;i >= 3;i -= 4){
 3         acc += shift_reg[i] * C[i] +
 4         shift_reg[i - 1] * C[i - 1] +
 5         shift_reg[i - 2] * C[i - 2] +
 6         shift_reg[i - 2] * C[i - 2] +
 7         shift_reg[i - 3] * C[i - 3];
 8     }
 9     for(;i >= 0; i--){
10         acc += shift_reg[i] * C[i];
11     }
代碼8.10

其中,第一個for循環將五次加法合在了一個語句中實現,解除了這四次乘加結果求和對上一個乘加結果的依賴,使得這四個乘加可以並行執行(乘法操作可由不同的硬件,如DSP48E模塊,同時分別完成)。當然,和前面的移位循環TDL一樣,運算次數不一定能夠整除並行因子4,需要後面一個for循環來完成最後的工作。如果實現指導循環次數爲4的整數倍,後面的for循環也完全可以省去。

當然,代碼8.10是爲了方便的說明解決乘加循環依賴關係的辦法給出的代碼,實際工程中,工程人員一般通過Vivado HLS開發工具提供的添加指令功能在源碼(在指令區中以#標誌)或專門的指令文件(在指令區中以%標誌)中添加編譯指令的方式,自動添加心慌並行展開指令。完成代碼8.10功能的編譯指令爲:#pragma HLS UNROLL factor=4。還可以通過在該指令中增加優化參數skip_exit_check來禁止高層次綜合器檢測並行化後循環是否完成(即代碼8.10中的第二個循環),從而提高代碼的效能。但這樣做的前提是實現知道循環次數剛好能被展開因子整除。

若在編譯指令中不給出展開因子factor,高層次綜合器將把循環全部展開,不再通過多個時鐘依次執行循環體,而是將每次循環都使用獨立的硬件電路展開執行。這樣將獲得最快的執行時間,也將消耗非常多的硬件。代碼8.11就是將乘加循環完全展開的代碼,同圖8.3.4是代碼8.11的綜合報告,可以看到設計延遲和吞吐量顯著降低,但消耗的硬件資源也大大增加了。

1 AC:
2     for(i = N-1;i >= 0;i--)    {
3 #pragma HLS UNROLL
4         acc += shift_reg[i] * C[i];
5     }
代碼8.11

 

圖8.3.4 優化後的綜合報告2 

需要注意的是,由於目標器件的資源是有限的,並非所有的展開都能實現,讀者需要根據項目目標器件和項目性能需求綜合取捨設計展開的而程度。

5、數據位寬優化

C語言本身定義了衆多的數據類型,如定點的char、short、int、long等,浮點的float、double等。但由於C語言最初是爲圖靈計算機設計的,現代通用計算機處理數據的寬度多爲8、16、32、64位等2的整數次冪,導致這些數據類型所對應的硬件也分別爲8、16、32、64位。對可編程邏輯器件而言,並沒有類似計算機這樣對位寬的限制。因此高層次綜合器的使用者可以根據算法的實際需要,定義任意寬度/精度的定點數據。

值得注意的是,高層次綜合器針對C和C++使用不同的庫和類型名稱來實現任意精度的定點數。C語言使用的頭文件名稱爲ap_cint.h(即需要在使用任意精度定點數的C源文件中包含#include “ap_cint.h”語句),使用的數據類型名稱爲intN或uintN,例如代碼8.12所示。

1 int7 Var1;
2 uint58 Var2;
3 ……
代碼8.12

C++使用的頭文件名稱爲ap_int.h(即需要在使用任意精度定點數的CPP源文件中包含#include “ap_int.h”語句),使用的數據類型名稱爲ap_int<N>或ap_uint<N>,例如代碼8.13所示。

1 ap_int<7> Var1;
2 ap_uint<58> Var2;
3 ……
代碼8.13

浮點數方面,高層次綜合器通過調用技術庫的方式提供浮點運算功能,因此所使用的浮點數的格式必須符合IEEE754規定的浮點格式,而不能任意改變float和double類型指數部分和小數部分所佔用的硬件寬度,否則將無法滿足技術庫中IP對數據格式的要求。

至於定點變量數據位寬的確定,可以遵循以下幾個簡單原則:

1)加法/減法的原則是,兩個位寬同爲n位的變量進行加法/減法後,結果佔用的位寬爲N+1位。

2)乘法的原則是,兩個位寬同爲n位的變量進行乘法後,結果佔用的位寬爲2×n位。

3)除法的原則是,一個位寬爲n位的變量除以一個位寬爲m位的變量後,若不考慮小數部分的話,結果佔用的位寬爲(n-m)位;若需要考慮使用定點小數表達結果,則可以根據除法結果需要的精度來確定結果佔用的位寬。

以代碼8.3和代碼8.4爲例,若輸入x爲12位補碼輸出ADC(數模轉換器)的轉換結果,則x可以定義爲int12數據類型,存儲歷史數據的shift_reg[N]也可以定義爲int12類型。係數數組C[N]的最大值和最小值分別爲500和-91,可以定義爲int9數據類型。acc需要存儲N=11個12位數據與9位係數乘積(12+9=23位)結果,最多爲23+4=27位(不小於log(11,2)的最小整數爲4)。若需要保留acc的所有計算精度,可見acc定義爲int27類型。

6、流水線優化

正如本章第一小節最後介紹的,如圖8.1.11所示,對高層次綜合器輸出結果的硬件電路優化的另一種有效方式,是通過“流水線”的方法提高硬件電路的並行性,從而提升電路處理數據的平均吞吐量性能。

但在默認情況下,高層次綜合器不會開啓流水線優化,只是順序完成高級語言給出的流程或循環任務。實際上在大多數情況下,在分解算法所需的硬件操作後可以發現某些“微步驟”之間不存在數據依賴關係,可以通過流水線方式並行化這些微步驟。例如FIR濾波器例子中的每個乘加操作可以分解爲讀取移位寄存器和係數數值,執行乘法操作和執行加法操作三個微步驟。對第一組兩個乘數的乘法操作與對第二組兩個乘數的讀操作之間不存在數據依賴關係,硬件電路完全可以在執行第一組數乘法的同時,讀取第二組數,從而大大節約算法的整體執行時間。同樣道理,執行加法操作的同時也可以執行後續數據的讀取和乘法操作。因此在最佳狀態下,可以實現圖8.3.4所示的乘加運算流水線,流水線中同時被處理的有三筆數據。

 

圖8.3.4乘加操作流水線示意圖 

高層次綜合器支持通過編譯指令實現對流水線的配置,在Vivado HLS環境右側的指令窗口中對代碼8.7中的MAC循環右擊添加指令,其指令格式爲:#pragma HLS PIPELINE II=2。其中PIPELINE代表這是一條流水線配置指令,字符串“II=2”中的數字代表循環起始間隔,也就是圖8.3.4兩次乘加執行的延遲時間。當然這個延遲時間數值不一定能夠實現,開始階段可以從大到小使用不同的數值進行嘗試,直到獲得最佳時間和資源效率。當然,由於本FIR濾波器中乘加操作的次數爲11,嘗試使用大於或等於11的數值是沒有意義的。如果對循環起始時間間隔沒有明確的要求只要獲得最佳性能,則可以在指令中省略這部分,將指令改寫爲#pragma HLS PIPELINE,高層次綜合器會嘗試幫助你找出最佳的數值。

代碼8.7中的TDL循環的循環體是進行移位操作,移位操作也可以分解爲讀取數據和移位寫入兩個微步驟,對TDL循環使用流水線優化指令也能獲得提升執行時間的效果。

圖8.3.5所示的是對代碼8.6中的MAC和TDL兩個循環都使用了流水線優化後的綜合報告,對比圖8.3.1得到的未做流水線優化後的綜合報告,可發現目標硬件的性能得到了很大提升。

 

圖8.3.5 優化後的綜合報告3

需要特別指出的是,循環展開和流水線優化對電路處理算法的優化作用不一定能夠疊加。原因是當循環被充分展開後,對每一筆數據的處理都有專門的硬件電路負責並行處理,這時再試圖通過流水線優化提高並行度的效果就不明顯了。對比展開和流水線優化兩種方式可以發現:展開是純粹的以硬件資源換處理時間——靠投入更多資源提升處理能力;流水線優化是通過調度現有資源工作時序,提升每個資源的工作效率實現的,需要增加的資源投入比較有限。

高層次綜合器是Xilinx可編程邏輯器件開發中更新最活躍的工具之一,自問世以來獲得了大量的關注,功能也發生了很大的改變。本章的介紹具有基礎性和引導性,試圖在較短的篇幅內幫助讀者建立高層次綜合器的基本概念,爲未來的進一步使用鋪平道路。但本章寫作中難免出現掛一漏萬和內容更新不及時的問題,建議需要深入該工具的讀者關注Xilinx官方網站不斷更新的應用筆記。 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章