關鍵字:ARM Cache 系統 優化 C語言 效率 功耗控制 系統架構 編譯器 efficient NEON
本文引用地址:http://www.eepw.com.cn/article/201611/317426.htmC編譯器並非無所不知
簡單地說, C編譯器並不能根據程式設計師的代碼就完全理解程式設計師的真實意圖,而且通常為了保證程序的正確執行,通常編譯器會做"最壞的"假設。最明顯和最著名的例子是"指針的混疊走樣"。這意味著編譯器必須做假設通過任何指針的寫都可能改變任何一個內存的地址,這對編譯器的優化有非常嚴重的影響。
其他的例子就是編譯器必須假定全局數據是易揮發的(volatile),在其他函數內,循環計數也是可能會隨時被修改的。好消息是在大多數情況下,程式設計師可以很容易給編譯器提供額外的信息來幫助編譯器優化。在其他情況下,你也可以改寫你的代碼以更好的表達你的意圖和更好的傳達特定的條件。例如如果你知道某一特定循環將總是至少執行一次,那麼do-while循環將會是比for(;;)是一個更好的選擇。這是因為對C語言的for循環在第一次迭代循環前需要測試是否終止。編譯器會因此被迫在兩個地方重複測試for的起始和結束,以保證功能的正確。也會你會說現代的分支預測硬體支持會減少這些循環前後的複雜的分支調整,但是總體上最好的還是通過給編譯器更多的指導來減少這些不必要的分支。ARM編譯器裡還有很多關鍵字來給代碼加上很多指導信息,如下面的__pure, __restrict以及__promise關鍵字。
__pure:關鍵字表明函數沒有負面影響,沒有對全局數據的訪問,即結果只取決於輸入參數,兩次相同的輸入得到的輸出也是相同的。
__restrict:該聲明用該指針指向區域的寫操作不會改變其他指針或者引用指向的數據。這個關鍵字對於循環優化尤為有用因為它增加了編譯器的自由度,編譯器就可以採取一些變換,如unroll等。
__promise:表明在程序的特定範圍內,某個條件一直為真,如下面例子中的表達式:
__promise intrinsic這裡告訴編譯器循環計數器在那個循環內,循環計數器是大於0的,並且能被8整除。這就能讓編譯器把for循環轉化為do-while,並且可以進行把循環展開至多8次而不用擔心循環邊界問題。這種方式尤其適用於NEON處理器的向量化操作。
C編譯器並非無所不能
C編譯器不能完全的理解程式設計師的意圖,同樣C編譯器也不是什麼事情都能做。C編譯器不能產生很多指令,尤其是最近ARM架構中引入的指令,這主要因為這些指令的語義跟C語言並不完全一致。熟練的程式設計師可以手工鞋彙編代碼來使用這些新指令,但是使用ARM C編譯器提供的豐富的intrinsic函數將更為簡單些。下面的例子是使用ARMv6以後引入的SMUSD和SMUADX指令實現的複數乘法,
一下的代碼是彙編的輸出
如果編譯器能inline內聯這些函數,也就沒有函數調用的開銷了,這也是使用內斂的函數實現相對於寫彙編的實現的優勢,即保持代碼的可移植性和可讀性。
NEON編譯器的NEON支持
C編譯器還能通過intrinsic函數和內聯的數據類型來直接訪問NEON多媒體處理器的操作。以下是一個數組乘法的直接實現,左邊的C代碼實現,右側的是對應的彙編語言。彙編代碼只列出了循環核。
下面的一對是相同的循環使用NEON intrinsics的實現和相應的彙編代碼。需要注意的是該循環已經展開4次來反映NEON的數據加載、乘法和存儲,每次處理都是4個32-bit的帶寬。這大幅降低了執行周期。而循環的額外開銷也由迭代次數降低而減少。
從以上的彙編,如果仔細看的話,你會發現編譯器並沒有產生和C代碼完全一致的代碼,這些指令的次序有所改變,這是編譯器為了減少interlock從而最大化吞吐。Interlock是由指令的流水線stall產生的。這也是使用intrinsic相對於手寫彙編的優勢,你可以利用編譯器的特性來把C代碼周邊的環境考慮進來做針對目標平臺的優化。
Data Cache使用
大多數應用程式員往往把cache當做作業系統OS層面需要考慮的問題。當然,cache的配置與管理是作業系統負責的,應用程式一般不允許幹涉cache操作。但這並不是說應用程式應該完全忽視系統還存在cache這個事實,理解cache的結構來優化代碼將可以提供巨大的性能提升。在寫代碼時考慮cache如何操作這些數據將利於代碼的性能一致性。
數據結構的對齊到cache行邊界將非常利於數據cache line的pre-load,cache需要基於數據訪問的時間和空間連續性,因而更新數據的時候是按照cache行來更新的,C編譯器提供了一個對齊數據到2的冪次的關鍵字如下所示:
int myarray[16] __attribute__((aligned(64)));
一些非常常見的算法還可以寫成cache友好(cache-friendly)方式以提高性能。眾所周知,當數據被連續訪問多次,這時cache的性能將非常高,因為這些連續訪問的數據此時已經在cache內了,可以被Core重用(當前,前提是此時的連續訪問的數據大小沒有超過cache的總大小)。像矩陣乘法這種常見的算法因為其數據訪問次序會給cache性能帶來一定的麻煩,下面是一個簡單的矩陣乘法函數的實現,
從實現中可以看出,數組a是被按照行連續訪問的因為其最右邊的索引變化最快,同理b數組也是連續訪問的,但是數組c確實按照列訪問的,這種按照列跳著讀取數據的方式確實不是cache友好的,因為這種按照列順次讀取的會經常更新cache數據因為會導致後面即將要用到的數據從cache空間被清除出去。雖然應用程式開發時,cache表現往往都是隱含的,但這種性能的損失確實會帶來功耗的增加,因為cache的miss導致對外存的訪問次數增加,而且這些訪問都是burst突發的,因而會增加DDR功耗。有些數據的訪問模式確實非常不利於cache的reuse,這時需要考慮其他的實現儘可能的避免這種數據訪問。如在一個write-allocate的cache系統中,大量數據的寫會讓cache裡堆滿了後面不會用到的數據,這些數據一般不會用到,當然一般的cache系統都是可配的read-allocate的。現在的一些高級的ARM cache控制器已經能夠處理這種write-allocate的情況,當出現大量的鞋操作時暫時關閉write-allocate模式,這種自動的調整cache參數是完全透明的,但是如果寫代碼時能考慮cache的特性,cache的架構,還是對高性能代碼非常有用的。
全局數據訪問
ARM構架的特點是你不能指定一個完整的32位的地址作為內存訪問的地址,這是由於ARM的指令字長決定的。因而通常訪問一個變量的內存地址需要被放置在一個寄存器或者至少一個起始地址在寄存器中然後加上一個簡單的偏移量。這導致了對於每個這樣的全局變量編譯器在編譯時必須在運行時存儲和加載基指針來訪問外部全局變量。如果一個函數訪問外部全局變量非常頻繁時,編譯器需要假定它們在獨立的編譯單元,因此不能確定在運行時這些全局變量是否能共享同一基址寄存器。因而每個全局變量都需要一個獨立的基址指針。如果你能讓編譯器推斷一群全局變量能共用一個存儲器基地址時,他們可以通過基址的不同偏移來訪問。要做到這一點,最簡單的方法就是縮小全局變量的範圍,只在需要用到的模塊裡聲明,然而不需要全局變量的應用程式少之又少,這並不是一個很切合實際的解決方案。最常見的解決方案是將全局變量或者相關的全局變量組成結構體。這些結構體在編譯時可以保證放在一個基址加偏移的地址的。
System power management系統功耗管理
現在我們轉到作業系統層次的更廣泛的系統問題。在大多數系統裡作業系統控制著比如時鐘頻率、工作電壓、單獨core的功率控制狀態等。應用程式通常不允許進行這些控制的。有一個最基本的關於功耗的問題一直廣為爭論:是先用最快的速度完成計算的工作,然後最長時間的進入休眠狀態還是把讓處理器一直工作在電壓和頻率都降低的低功耗狀態下更為節約功耗。現在這些爭論往往更著眼於日益增長的系統的靜態功耗。從歷史上看,靜態功耗(主要是滲漏)已經大大小於動態功率的消耗。然而晶片結構變得越來越小,洩漏的增加這一事實使的靜態功耗日益成為能耗的主要貢獻者。現在的結論就是最好是迅速完成任務,然後關機停止(避免洩漏),而不是繼續執行更長的時間。
一個合理的尺度
我們需要的是一個度量來結合功耗和一個特定的計算需要的運行時間。這樣一個度量常常被稱為"能量延遲積"或EDP(Energy Delay Product.圖3所示)。雖然這樣的度量標準已經廣泛應用於電路設計很多年,但目前軟體開發領域尚無公認的方法來推導或使用這樣一種度量。
圖3.能量延遲積
上面的例子顯示[2]在決定cache緩存大小上EDP度量所起的輔助作用。很明顯一個更大的緩存會增加功耗。然而EDP度量表明有一個的在64KB大小附近有一個比較合理的位置能獲得更高的性能和功耗平衡。
管理子系統sub-systems
在一個單晶片系統裡我們必須確保額外的計算引擎(如NEON)與外部外設(串口和類似的設備)只在需要的時候才啟動。這是作業系統開發者需要考慮的調度問題,也是晶片廠商需要提供管理這些設備的特性。作業系統幾乎都需要根據特定的硬體平臺進行定製,例如飛思卡爾的i.MX51晶片包含一個NEON的監控器,黨用不到NEON時會自動關閉。當碰到沒有定義的指令時會通過中斷喚醒該協處理器。
在多核系統,我們可以自己選擇開關單一的核心以匹配系統的負載需求。單一Core的關閉開啟都是系統決定的,現在的ARM對稱多核SMP Linux支持一下特性:
1)CPU熱拔插hotplug;
2)負荷平衡以及動態的優先級調整;
3)智能並且cach優化的調度算法;
4)每個cpu core都能動態電壓和頻率調整Dynamic Voltage and Frequency Scaling (DVFS);
5)每個CPU都有獨立的功耗狀態管理機制;
內核為通用的外部電源管理控制器配置了一個接口。這個接口需要針對特定平臺臺來選擇可使用的特性。如TI的OMAP4平臺提供了再一個範圍的電壓和頻率間調整的選項,通過運行評分("Operating Performance Points")系統會自動選擇最適合的功耗方案。這樣設備的功耗根據系統負載不同可以從600微瓦到600 mW。
程式設計師需要做什麼
在多核系統中,硬體的高性能也許讓我們決定一切都交給作業系統把,然而在寫代碼和配置作業系統時如果能考慮如下因素是非常重要的。
1)系統效率(System efficiency):智能和動態的任務優先級調度;負載平衡;
2)計算效率(Computation efficiency):數據,任務和函數級別的並行;減少同步開銷overhead
3)數據效率(Data efficiency):有效利用存儲系統特性,謹慎維護cache一致性以避免cache顛簸和錯誤的core間共享。
總結
1)合理配置工具和硬體平臺;2)仔細寫代碼和合理配置配置cache以儘可能減少外部內存訪問;3)速度優化以及合理利用NEON等運算加速器以減少指令執行數;