針對如何為目標平台實現最佳化程式碼的技巧,經驗豐富的嵌入式系統程式設計人員大多數了然於心,但許多近來才接觸嵌入式系統編程的設計人員往往僅根據其於較少限制的平台上所習得的編程技巧。

除了演算法的一般效率,我們通常無需考慮太多硬體細節,但對於嵌入式應用則必須特別小心謹慎。除了效能外,還要考慮目標硬體(特別是可用的記憶體容量與類型)的具體限制。這些系統中的記憶體──通常明顯小於您的期望──必須容納整個軟體及所有的操作數據。

CEVA, suitcase

圖1:嵌入式系統中的記憶體就像是必須容納所有軟體和可用資料的行李箱,但「這卡皮箱」通常比你預期的更小。(來源:CEVA)

而在這些系統中還存在多種類型的記憶體,可能使得考慮更加複雜。為了簡化編程,您只會看到一個邏輯記憶體空間,但在硬體中可能以多種不同方式建置位址範圍。其中某些屬於外接主記憶體,只能從處理器以一或多階快取記憶體存取。

通用的硬體最佳化會採用另一個記憶體區域做為緊密耦合記憶體(TCM),後者通常緊鄰位於同一晶片的處理器旁。TCM確保為儲存於該記憶體中的任何指令或數據提供單一時脈週期存取,標準記憶體則只對已存在於快取記憶體的指令/數據,或先將指令/數據轉到主記憶體才能提供相同的效能,否則可能需要更多時脈週期。TCM是(記憶體映射)高速晶載記憶體(on-chip memory)的一個例子;TCM的其他用途包括影像處理中高速存取的影像緩衝器。

另一個考慮因素是晶載記憶體可以降低功耗,而轉入主記憶體則因需以更大的電流驅動晶片間封裝接腳和電路板互連而提高了功耗;這是低功耗應用的一個重點。

為什麼不使用大的晶載記憶體以及從外接記憶體減少加載/儲存的頻率?遺憾的是,因為大型晶載記憶體會大幅增加晶片面積,從而提高成本、削弱競爭力。系統架構工程師必須非常謹慎地權衡效能與成本,以決定提供16KB或1MB的TCM。這使程式設計人員必須擔負更多責任,決定如何使用或計畫這些記憶體(當然您必須對晶片架構擁有前期發言權),尤其是要決定讓哪些功能或數據使用高速記憶體。

某些該做的事明顯可見。假設您已著手於PC型應用或為早期產品開發應用,且看來對DSP很感興趣,那麼您可能計畫進行大量的浮點運算。請盡可能將數據類型從雙精度改為單精度,這樣就可能將數據量減少一半。

暫存記憶體池(scratch memory pools)是一次劃分大塊記憶體的方法,用於多個較小但相關分配的方法;這在高速分配和解除分配方面很受歡迎,但所費不貲。只要這些記憶體非屬平行使用,即請盡量將它們合併於單一記憶體池,或盡量返回傳統的堆上malloc;這樣會使速度減緩,但會大幅提高記憶體效率。

特別是當涉及TCM時,請對程式碼進行整體分析,以查找耗費最多運行時間的功能,然後從需求最高的功能往下逐一決定應納入TCM者。您當然必須有所取捨,例如當高時間需求的功能調用低時間需求的功能時,是否值得將後者從快取記憶體移出?對快取記憶體的匹配命中率很高或可忍受不常發生的更長延遲時,這未嘗不可。

在盡量壓縮運行所需程式碼和數據時,一個基本的技巧就是將各種除錯、分析和日誌程式碼都納入可在運行所需程式碼時停用的標注。對PC程式碼而言可能無足輕重(尤其是要在實際運行軟體上執行除錯器時),但在此處即為關鍵;您甚至應在停用該程式碼的情況下進行回歸測試。除錯中只要出現一個被忽略的運行時相依性,即可能成為下游的夢靨。

同樣地,必須確保軟體中的每一個程式碼位元都被加以利用。請執行覆蓋率測試,但在刪除所找到的未被使用程式碼(可能是早期版本所遺留,現已無用的程式碼)時務必小心。它可能是對某個罕見且不容忽視情況的錯誤排除,或應包含在回歸測試中但很難直接觸發的程式碼;請先與架構師和硬體團隊後討論再決定。

最後,請與架構工程師(若果需要的話,還包括行銷部門)討論他們要求的功能哪些是不可或缺;他們可能無法瞭解,在竭盡一切最佳化手段後程式仍可能無法納入可用的記憶體,以至於不得不犧牲某些他們認為非常酷的功能,或按您所提供的額外記憶體容量資料,再向企業團隊要求更大的晶載記憶體;這二者都會增加您的價值!

以上討論有關如何編程和設計數據結構以最佳化嵌入式系統程式碼大小、效能和功耗的技巧,對於習於64位元系統及十億位元組(gigabyte)記憶體等現代電腦配備的我們,幾乎已將早期電腦不可或缺的軟體壓縮拋諸腦後;但嵌入式系統必須兼顧程式碼功能和記憶體容量有限的要求,又將我們帶回到必須重新掌握此一技巧的未來。

CEVA, toobox

圖2:為複雜的嵌入式DSP應用實現最佳化,取決於軟體設計與資料結構,同時還必須謹慎地使用編譯器與鏈接工具。(來源:CEVA)

大多數的最佳化取決於精巧的程式碼設計和微調,但建構工具──尤其是編譯器和鏈接器(Linker)──紅花綠葉的襯托也不可小覷。接下來將以CEVA-Toolbox作為討論在這些步驟中可使用的選項。在種種情況下,程式碼大小都是最主要的限制條件,因此我們將從專注於討論如何為其實現最佳化。

編譯器選項

在設計和除錯程式碼時,幾乎都會使用‘g’選項來產生除錯資訊。執行‘g’選項能夠避免編譯器執行任何可能變更程式碼的最佳化,導致除錯複雜化。因此,當你在仔細進行程式碼最小化時請停用該選項。

如何選擇編譯器的最佳化是第二個考慮因素。預設編譯器將以多種方法來最佳化效能,其一是為迴圈的每次迭代複製程式碼,以展開相對較小的‘for-loops’,這是以佔用較多記憶體為代價,減少了每次迭代時耗用於設置及測試迴圈索引的負載。請使用‘Oz’選項避免展開,改為程式碼較小、速度稍慢的程式。

編譯器最佳化效能的另一種選擇是內嵌(inline)某些特定函數(特別是小函數),以消除從堆疊推入和彈出引數以及在函數間跳轉所耗用的負載;其代價是程式碼大小會因多次調用該函數而擴大。使用‘-INLINE = no’選項可停用自動內嵌。

另一個最佳化選項對傳統平台似乎無足輕重,但對DSP的程式碼則影響重大。那就是盡可能停用編譯器對指標混疊的保護(若適用)。這種保護的目的是在DSP等超長指令字(VLIW)裝置上平行化一組指令時,編譯器將確保這些指令中的任何混疊參照在有一個以上指向同一數據時不會產生競爭狀態,以便限制某些指令可平行運行程度。請用‘-alias = restrict’選項強制限制這樣的解釋以便進行較多的平行性推論。前提當然是必須仔細檢查以及充份地回歸分析,以確保這種解釋的安全性。

鏈接器

鏈接器也可以最佳化程式碼大小,其中之一是移除未引用的功能(但務必謹慎)。某些功能可用數據指標調用、直接跳轉到硬程式碼位址,而中斷分析服務則常以傳統的呼叫協議來存取;此一選項因此必須考慮多種可能性。它可自動啟用或用‘-keepUnrefFuncs’選項停用。

另一種鏈接器最佳化可以進一步壓縮某些符號中未被組譯器解析,而在鏈接時尋址的程式碼。如果符號在程式開始時未被解析,則組譯器必須賦予最大可能的空間來對目標處理器定址,以至於在鏈接器最終解析時造成浪費。如果不採取特殊手段,許多這樣的符號實際位址可能非常小,卻佔用非常大的位址字節,縮小後者因此可顯著壓縮整個程式碼。這同樣必須非常小心:在縮小任何給定位址時,程式碼中對該符號後任何位置的直接引用都必須調整,並考慮對數據調整的要求(有時視處理器而異)。每次壓縮都必須盡可能縮小程式碼,以兼顧節省空間和對其餘程式碼的影響;還好這些最佳化都會預設執行。

謹慎使用這些編譯器和鏈接器選項,同時搭配最佳編程實務,有助於進一步縮小程式碼和數據,以兼顧經濟性和效率的方式滿足嵌入式系統的需求,從而讓經驗豐富的嵌入式系統程式設計人員進一步提升價值,達成理想的目標!

本文同步刊登於電子技術設計雜誌2019年11月號