天天看點

《高性能科學與工程計算》——2.3 小方法,大改進

本節書摘來自華章計算機《高性能科學與工程計算》一書中的第2章,第2.3節,作者:(德)georg hager gerhard wellein 更多章節内容可以通路雲栖社群“華章計算機”公衆号檢視。

2.3.1 消除常用子表達式

消除常用子表達式經常被認為是編譯器的任務。其基本思想是,在構造複雜表達式之前,預先計算其中被多次調用的子表達式,并将結果存儲在臨時變量中。在循環代碼優化中,這個方法稱為循環無關代碼移出:

《高性能科學與工程計算》——2.3 小方法,大改進

該優化方法可節省大量計算時間,特别是當子表達式中包含“強”操作(如sin())時。盡管子表達式消除可能會受其他代碼的影響,編譯器原則上能夠檢測到并進行有關優化工作。然而,如果這個操作還需要其他關聯規則,那麼編譯器常常不會進行優化(2.4.4節詳細讨論了編譯器優化和算術表達式重排序優化)。在實際應用中,手工完成這項工作是非常好的政策。

2.3.2 避免分支

“緊”循環(比如,循環體内部操作很少)常用的優化技術是軟體流水(見1.2.3)、循環展開和其他優化技術(見本節後面内容)。由于某些原因編譯器自動優化失敗或者優化不夠充分,則會明顯影響程式性能。如當循環體内部包含條件分支時,則很容易發生:

《高性能科學與工程計算》——2.3 小方法,大改進

上面的矩陣向量乘執行個體,使用if表達式完成了對上三角矩陣(sign = 1)、下三角矩陣(sign = -1)以及對角線元素(sign = 0)的分别處理。一旦處理器遇到對應的條件分支,許多分支預測邏輯單元在計算結果可用之前就會采用基于統計的方法對該計算結果進行預測。一旦預測被證明是錯誤的(也稱為分支預測失誤或分支迷失),流水線将重新回到該分支位置,這意味着時鐘周期的浪費。此外,分支預測失敗後,編譯器也就不能繼續進行循環展開或者simd向量化(見下一節内容)等後續優化。幸運的是,該循環嵌套可通過改進消除所有if表達式:

《高性能科學與工程計算》——2.3 小方法,大改進

通過使用兩個不同的内循環,條件分支被移出。要說明的是,這個循環嵌套還有更多的優化潛力。具體請參考第3章對資料訪存操作優化的讨論。

2.3.3 使用simd指令集

盡管向量處理器也使用simd指令,微處理器對simd指令的使用也稱為“向量化”,使用simd指令集更類似于現代向量化系統的多軌機制。一般而言,如果一條單一指令可執行更多的操作,那麼一個上下文中“可向量化”循環的性能可以更高。例如,盡可能使用“小”資料類型。從dp切換到sp可能會導緻兩倍的性能提升(具備simd能力的x86型cpu[v104, v105]),而且還可以将更多的資料加載到cache中。

當然,選擇simd指令,并不總能帶來性能提升。如果應用程式性能嚴重受限于受訪存帶寬,不采用simd技術可彌補這一差距。使用simd指令,隻會大大加快寄存器到寄存器操作的性能,但是會大大延長寄存器從記憶體子系統中擷取新資料的時間。

圖1-8描述的一條單精度加法指令可用在數組相加的循環中:

《高性能科學與工程計算》——2.3 小方法,大改進

在上例中,循環的每次疊代都是互相獨立的,循環體内部沒有條件分支,資料訪存也為連續訪存操作。然而,使用simd指令需要對循環代碼(如上例中應用的)重新組織:多次疊代(與simd寄存器大小相等)間不允許有分支,能夠像單一的“塊”一樣執行。即使沒有simd,這也是一個衆所周知的優化方法——循環展開(詳細讨論見3.5節)。因為循環的疊代次數一般不是寄存器大小的整數倍,是以餘下的循環疊代還是會标量執行。忽略軟體流水(參見1.2.3節)的僞代碼如下:

《高性能科學與工程計算》——2.3 小方法,大改進

https://yqfile.alicdn.com/428b7bd8ad3bed6d60ab45cbfdeb5df25f20987a.png

" >

r1、r2、r3都是128位的simd寄存器。理想情況下,上述操作由編譯器自動實作。實際優化中,可使用編譯指導語句,提示可向量化的代碼(這些代碼的向量化必須是安全并且有益的)。

在這個例子中,simd 讀取和存儲指令需要特别關注。操作對齊資料和非對齊資料的一些simd指令集是不同的。以x86(intel/amd)架構為例,該架構擁有對齊和非對齊的“打包”sse 讀取和存儲指令[v107,o54]。如果将對齊的讀取和存儲指令應用于非對齊記憶體位址(不是16的倍數),則會抛出異常。如果編譯器對應用到向量化循環中的數組的對齊情況一無所知,又不能對其影響和控制,盡管會帶來性能損失,也一定要使用非對齊的讀取和存儲指令(或者使用一系列的标量化指令)。如果程式員不能肯定資料是對齊的,則強制編譯器假設最優對齊是非常危險的。在某些架構上,對齊操作至關重要,需要盡一切努力保證讀取和存儲指令對齊到合适的位址邊界。

疊代間存在真依賴的循環(見1.2.3節)是不能被simd以此種方式向量化的(然而也有轉機,見習題2.2)。

《高性能科學與工程計算》——2.3 小方法,大改進

這裡,編譯器将會進行标量操作,也就意味着隻使用simd寄存器的最低位(x86架構)。

值得注意的是,循環向量化沒有固定的指導原則。一個(可能是最弱的)可能的定義是循環内所有算術運算的執行要充分利用simd寄存器(完全利用simd寄存器的寬度)。即便如此,仍然可以使用标量讀取和存儲指令,編譯器也會認為這樣的循環是向量化的。支援sse的x86處理器,其寄存器的高64位和低64位可以獨立使用。是以,上述循環的向量化加法可看作為雙精度加法:

《高性能科學與工程計算》——2.3 小方法,大改進

上例中,如果操作數駐留在cache中,該版本并不能提供最佳性能。盡管算術運算(第9行)都是simd并行操作,而讀取和存儲卻都是标量運算。由于缺乏完整的編譯器報告,是以确定這樣一個缺陷的唯一方法是手動檢查生成的彙編代碼。即使添加指令行選項或者源代碼指導語句,編譯器還是不能有效完成循環的向量化。那麼,在使用彙編語言之前,一個“不得已而為之”的方法是使用編譯器内部函數(compiler intrinsics)。内部函數和彙編指令非常相似,兩者可被編譯器進行1:1轉換。由于編譯器為内部函數提供了可映射到simd操作數的特殊資料類型,是以使用内部函數可使使用者從追蹤每個寄存器使用情況的繁重工作中解脫出來。内部函數不僅對向量化非常有用,而且在進階語言設計不能很好地映射到cpu某些特性的情況下,也非常有用。然而不幸的是,即使在同一個硬體架構上,編譯器間的内部函數也互不相容[v112]。

最後,必須強調的是,相對于真正的向量化處理器,risc系統并不總能從向量化中受益。如果一個訪存受限程式可使用寄存器或cache進行大量資料重用(見第3章例子),那麼資料重用優化的潛在性能提升是非常大的,甚至可以放棄向量化優化。