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