ARM高效C編程和優化--編譯器,內存和Cache優化以及功耗管理

2020-12-18 電子產品世界
上節主要介紹在資源受限的ARM設備上,在各種類型的作業系統上的選擇,在C語言編程角度,如何構建代碼才能更好的指導編譯器compiler進行優化,諸如數據對齊data alignment,數據類型data type的選擇,C語言函數調用的參數傳遞方式,以及編譯器對結構體和數組的基本處理方式,下節則主要介紹編譯器的使用規則,如何指導編譯器進行合理的優化,以及系統級的NEON優化,從cache使用到系統功耗控制等。

關鍵字:ARM Cache 系統 優化 C語言 效率 功耗控制 系統架構 編譯器 efficient NEON

本文引用地址:http://www.eepw.com.cn/article/201611/317426.htm

C編譯器並非無所不知
簡單地說, 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等運算加速器以減少指令執行數;

相關焦點

  • ARM平臺NEON指令的編譯和優化
    如何編譯和優化,包含如何向量化、向量化的ARMCC和GCC編譯器選項、NEON的彙編和EABI程序調用規範、如何在bare-metal和Linux作業系統上檢測NEON硬體、如何指導編譯器進行向量化NEON指令的優化等內容。
  • ARM Cortex系列(A8/A9/A15/A7) NEON多媒體處理SIMD引擎優化
    本文介紹了NEON處理器的基本架構、NEON處理器的並發情況下Cortex-A8和Cortex-A9的區別、NEON的寄存器組和數據類型、NEON編程的針對編譯器、彙編器的優化方法以及其他的提高性能的並行方法。
  • c編譯器so easy,gcc c編譯器生成、使用動靜態庫
    第一章程序開發人員大多接觸過c編譯器,請注意,不要將c編譯器和編輯器弄混淆哦。本文對c編譯器的講解,同樣基於gcc c編譯器,本文主要目的在於對linux環境下gcc c編譯器生成和使用靜態庫和動態庫予以介紹。此外,本文為系列教程第一篇——基本概念篇,之後將帶來另外兩篇。
  • ARM處理器NEON編程及優化技巧——處理剩餘的元素
    ARM的NEON協處理器技術是一個64/128-bit的混合SIMD架構,用於加速包括視頻編碼解碼、音頻解碼編碼、3D圖像、語音和圖像等多媒體和信號處理應用。本文主要介紹如何使用NEON的彙編程序來寫SIMD的代碼,包括如何開始NEON的開發,如何高效的利用NEON。首先會關注內存操作,即如何變更指令來靈活有效的加載和存儲數據。
  • 妙到顛毫:你應該學會的 bigcache 優化技巧
    fast cache service with millions of entries in Go[3]做的一些調研和總結。,訪問它們需要網絡的開銷,延時無法保障,作者需要一個進程內的基於內存的 cache 庫。
  • ARM處理器NEON編程及優化技巧——數據加載和存儲
    本文主要介紹如何使用NEON的彙編程序來寫SIMD的代碼,包括如何開始NEON的開發,如何高效的利用NEON。首先會關注內存操作,即如何變更指令來靈活有效的加載和存儲數據。接下來是由於SIMD指令的應用而導致剩下的若干個單元的處理,最後是一個例子來說明用NEON來進行SIMD優化。
  • ARM處理器NEON編程及優化技巧——矩陣乘法的實例
    ARM的NEON協處理器技術是一個64/128-bit的混合SIMD架構,用於加速包括視頻編碼解碼、音頻解碼編碼、3D圖像、語音和圖像等多媒體和信號處理應用。本文主要介紹如何使用NEON的彙編程序來寫SIMD的代碼,包括如何開始NEON的開發,如何高效的利用NEON。首先會關注內存操作,即如何變更指令來靈活有效的加載和存儲數據。
  • 誰說國產編譯器沒救了?這個 C/C++ 和 JavaScript 編譯器來了|程序...
    自述 | 楊曉兵編輯 | 伍杏玲編者前記:編譯器是連接人類世界與機器世界之間的一座橋梁,它可將程式設計師理解的高級語言,轉換成程序高效執行的機器碼。在 C/C++ 編譯器裡,有 VC、Borland C++、GCC、Watcom C/C++ 等國外熱門編譯器,但屬於國內自主研發的編譯器較少。畢竟開發一款實用的編譯器不易,涉及前端詞法、語法分析、語意分析、大量的編譯優化等工作。而有一支團隊,不惜花費十餘年精力完全自主研發出一款 YC 編譯器和 YC 瀏覽器內核。為何他們不遺餘力地自主研發編譯器和瀏覽器內核?
  • 華為方舟編譯器正式支持C語言:完全開源
    2019年8月底,華為方舟編譯器(OpenArkCompiler)正式開源,邁出了跨越性的一步。一年多來,方舟編程體系陸續實現了編譯器、引擎、調試器的開源,其中編譯器的重點功能主要集中在Java應用程式靜態編譯上。
  • Zig 0.7.1 發布,想要替換 C 的程式語言
    其中涉及到標準庫、編譯器、構建系統、compiler-rt、zig cc 和 zig c++ 等內容。從 release notes 可以看到,此版本修復的問題集中在編譯器上,這不難理解,因為上個版本發布時,團隊指出 0.7.0 的主要目標之一正是實現自託管編譯器。
  • android平臺arm指令學習和調試
    force-modearm或者setarmforce-modethumb讓gdb切換thumb和arm代碼顯示。Gdbarm和ida反彙編比較:4)模塊基地址的獲取cat/proc/pid/maps找到5)Dump內存的命令式dumpbinarymemoryc:\xxxstartAdendAd6)小細節要注意的是:.init_proc的地址是0x31cb9,ida直接點過去,是一堆數據,摁c無法轉成代碼因為arm指令和thumb指令是2位元組或者
  • ARM晶片是物聯網主流?對比X86、ARM、MIPS架構後我們找到了原因
    指令集可分為複雜指令集(CISC)和精簡指令集(RISC)兩部分,代表架構分別是x86、ARM和MIPS。ARM、X86、MIPS這三大架構大家都不陌生, Intel因為普及於臺式機和伺服器而被人們所熟知,MIPS在32位和64位嵌入式領域中歷史悠久,獲得了不少的成功,而在移動網際網路時代ARM無疑成為霸主。
  • ARM內存地址訪問
    1、訪問絕對地址的內存位置: #define pISR_EINT0 (*(unsigned *) (_ISR_STRATADDRESS+0x74))本文引用地址:http://www.eepw.com.cn
  • C語言編譯器哪個好?6款好用的C語言編譯器推薦
    它使用MingW32/GCC編譯器,遵循C/C++標準。開發環境包括多頁面窗口、工程編輯器以及調試器等,在工程編輯器中集合了編輯器、編譯器、連接程序和執行程序,提供高亮度語法顯示的,以減少編輯錯誤,還有完善的調試功能,能夠適合初學者與編程高手的不同需求,是學習C或C++的首選開發工具!
  • 一文看懂arm架構和x86架構有什麼區別
    本文主要介紹的是arm架構和x86架構的區別,首先介紹了ARM架構圖,其次介紹了x86架構圖,最後從性能、擴展能力、作業系統的兼容性、軟體開發的方便性及可使用工具的多樣性及功耗這五個方面詳細的對比了arm架構和x86架構的區別,具體的跟隨小編一起來了解一下。
  • 如何利用 TVM 優化深度學習GPU op?教你用幾十行Python代碼實現2-3...
    ,和 NNVM 一起組成深度學習到各種硬體的完整優化工具鏈,支持手機,cuda, opencl, metal, javascript 以及其它各種後端。='DepthwiseConv2d')通用 GPU 優化指南胡玉煒在文章中提到了優化 CUDA 代碼時通常需要注意的三大問題,即數據重用(data reuse)、共享內存(shared memory)和訪問衝突(bank conflicts)。
  • C編譯器小家族之C編譯器各顯神通
    GCCGCC原名GNU C Compiler,後來逐漸支持更多的語言編譯(C++、Fortran、Pascal、Objective-C、Java、Ada、Go等),所以變成了GNU Compiler Collection(GNU編譯器套裝),是一套由GNU工程開發的支持多種程式語言的編譯器。
  • C 翻身?微軟重寫開源的 C 編譯器!
    「我們把所有對於語言正確性和性能的要求都集中在一份代碼中,使其擁有最佳的質量和最好的多樣性——我們將重新定義「編譯器」這個詞。」Roslyn是C#和Visual Basic.NET的開源編譯器的項目名。
  • JAVA並發編程:並發問題的根源及主要解決方法
    編譯器優化帶來的有序性因為現在程式設計師編寫的都是高級語言,編譯器需要將用戶的代碼轉成CPU可以執行的指令。同時,由於計算機領域的不斷發展,編譯器也越來越智能,它會自動對程式設計師編寫的代碼進行優化,而優化中就有可能出現實際執行代碼順序和編寫的代碼順序不一樣的情況。