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