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