作者:IAR Systems 在嵌入式開(kāi)發(fā)中,代碼的體積和運行效率非常重要,代碼體積往往和芯片的FLASH、RAM容量對應,程序的運行效率也要求在相應能力的處理器上運行。在大多數情況下,成熟的開(kāi)發(fā)人員都希望降低代碼體積、提高代碼運行效率,然而具體該怎么做呢?本篇文章將以國際知名編譯器廠(chǎng)商IAR Systems的編譯器為例,來(lái)解答開(kāi)發(fā)人員在實(shí)際工作中常常遇到的問(wèn)題,工程師朋友們可以在IAR編譯器上進(jìn)行實(shí)踐驗證。 對于嵌入式系統,最終代碼的體積和效率取決于由編譯器生成的可執行代碼,而非開(kāi)發(fā)人員編寫(xiě)的源代碼;但是源代碼的優(yōu)化,可以幫助編譯器生成更加優(yōu)質(zhì)的可執行代碼。因此,開(kāi)發(fā)人員不僅要從整體效率等因素上去構思源代碼體系,也要高度關(guān)注編譯器的性能和編譯優(yōu)化的便捷性。 有優(yōu)化功能的編譯器可生成既小又快的可執行代碼,編譯器是通過(guò)對源代碼的重復轉換來(lái)實(shí)現優(yōu)化。通常,編譯器優(yōu)化會(huì )遵循完善的數學(xué)或邏輯理論基礎。但是某些編譯優(yōu)化則是通過(guò)啟發(fā)式的方法,經(jīng)驗表明,一些代碼轉換往往會(huì )產(chǎn)生更好的代碼,或者開(kāi)拓出進(jìn)一步編譯優(yōu)化的空間。 編譯優(yōu)化只有少數情況依賴(lài)于編譯器的黑科技,大多數時(shí)候編寫(xiě)源代碼的方式?jīng)Q定了程序是否可以被編譯器優(yōu)化。在某些情況下,即使對源代碼做微小改動(dòng)也會(huì )對編譯器生成的代碼效率產(chǎn)生重大影響。 本文將講述在編寫(xiě)代碼時(shí)需要注意的事項,但我們首先應明確一點(diǎn),我們沒(méi)有必要盡量減少代碼量,因為即使在一個(gè)表達式中使用 ?:- 表達式、后增量和逗號表達式來(lái)消除副作用,也不會(huì )使編譯器產(chǎn)生更有效的代碼。這只會(huì )使你的源代碼變得晦澀難懂,難以維護。例如在一個(gè)復雜的表達式中間加入一個(gè)后增量或賦值,則在讀代碼的時(shí)候很容易被忽略。請盡量用一種易于閱讀的風(fēng)格來(lái)編寫(xiě)代碼。 循環(huán) 下面看似簡(jiǎn)單的循環(huán)會(huì )報錯嗎? for (i = 0; i != n; ++i) { a = b; } 雖然不會(huì )報錯,但其中有幾點(diǎn)會(huì )影響到編譯器生成的代碼效率。 例如,索引變量的類(lèi)型應與指針相匹配。 像 a 這樣的數組表達式實(shí)際上是 *(&a[0]+i*sizeof(a[0]),或者通俗地說(shuō):將第 i個(gè)元素的偏移量加到 a 的第一個(gè)元素的指針上。對于指針運算, 索引表達式的類(lèi)型最好與指針所指向的類(lèi)型一致(__far 指針除外,因為其指針所指向的類(lèi)型和索引表達式的類(lèi)型不同)。如果索引表達式的類(lèi)型與指針所指向的類(lèi)型不匹配,那么在把它與指針相加之前,必須將它強制轉換為正確的類(lèi)型。 如果在應用中,堆?臻g資源(堆棧一般放在RAM中)比代碼尺寸資源(代碼一般放在ROM或者Flash中)更寶貴,則可以為索引變量選擇一個(gè)更小的類(lèi)型來(lái)減少堆?臻g的使用,但這往往會(huì )犧牲代碼尺寸和執行時(shí)間(代碼尺寸變大,執行時(shí)間變慢)。不僅如此,這種轉換也會(huì )妨礙循環(huán)代碼的優(yōu)化。 除上述問(wèn)題外,我們也要關(guān)注循環(huán)條件,因為只有在進(jìn)入循環(huán)之前可以計算出迭代次數的情況下,才可以進(jìn)行循環(huán)優(yōu)化。然而,這項計算工作非常復雜,并非用最終值減去初始值并除以增量那么簡(jiǎn)單。例如,如果 i 是一個(gè)無(wú)符號字符,n 是一個(gè)整數,而 n 的值是 1000,那么會(huì )發(fā)生什么情況?答案是變量 i 在達到 1000 之前就會(huì )溢出。 雖然程序員肯定不想要一個(gè)無(wú)限循環(huán),重復地將 256 個(gè)元素從 b 復制到 a,但是編譯器無(wú)法了解程序員的意圖。它必須假設最壞的情況,并且不能應用需要在進(jìn)入循環(huán)之前提供行程數的優(yōu)化。此外,如果最終值是一個(gè)變量,您還應該避免在循環(huán)條件中使用關(guān)系運算符 <= 和 >=。如果循環(huán)條件是 i <= n,那么 n 有可能是該類(lèi)型中可表示的最高值,因此編譯器必須假定這是一個(gè)潛在的無(wú)限循環(huán)。 別名 通常,我們不建議使用全局變量。這是因為您可在程序的任何地方修改全局變量,并且程序會(huì )因全局變量的值而變化。這就會(huì )形成復雜的依賴(lài)關(guān)系,使人很難理解程序,也很難確定改變全局變量的值會(huì )對程序產(chǎn)生怎樣的影響。從優(yōu)化器的角度來(lái)看,這種情況更糟糕,因為通過(guò)指針的存儲就可以改變任意全局變量的值。如果能通過(guò)多種方式訪(fǎng)問(wèn)一個(gè)變量,這種情況就會(huì )被稱(chēng)為別名,而別名使代碼更難優(yōu)化。 char *buf void clear_buf() { int i; for (i = 0; i < 128; ++i) { buf = 0; } } 盡管程序員知道向 buf 所指向的緩存區進(jìn)行寫(xiě)操作不會(huì )改變這個(gè)buf變量本身,但編譯器還是不得不做最壞的打算,在循環(huán)的每一次迭代中從內存中重新加載 buf。 如果將緩存區的地址作為參數傳遞,而不是使用全局變量,則可以消除別名: void clear_buf(char *buf) { int i; for (i = 0; i < 128; ++i) { buf = 0; } } 使用這個(gè)解決方案后,指針 buf 就不會(huì )被通過(guò)指針的存儲影響。如此一來(lái),指針 buf 在循環(huán)中就可以保持不變,其值只需在循環(huán)前加載一次即可,而不是在每次迭代時(shí)都要重新加載。 然而,如果需要在不共享調用者/被調用者關(guān)系的代碼段之間傳遞信息,則直接使用全局變量即可。但是,對于計算密集型任務(wù),尤其是涉及指針操作時(shí),最好使用自動(dòng)變量。 盡量不用后增量和后減量 在下文中,關(guān)于后增量的所有內容也適用于后減量。C 語(yǔ)言中關(guān)于后增量語(yǔ)義的標準文本指出:“后綴 ++ 運算符的結果是操作數的值。在得到結果后,操作數的值會(huì )遞增”。雖然微控制器普遍擁有可在加載或存儲操作后增加指針的尋址模式,但其中只有很少能以同樣的效率處理其他類(lèi)型的后增量。為符合標準,編譯器必須在執行增量之前將操作數復制到一個(gè)臨時(shí)變量。對于直線(xiàn)代碼來(lái)說(shuō),可以從表達式中取出增量,然后放在表達式之后。比如以下表達式: foo = a[i++]; 可以改為 foo = a; i = i + 1; 但如果后增量屬于 while 循環(huán)中的條件,又會(huì )發(fā)生什么?由于在條件后面沒(méi)有可以插入增量的地方,因此必須在測試前添加增量。對于這些常見(jiàn)但是又與生成可執行代碼效率密切相關(guān)的設計,諸如IAR Systems的Embedded Workbench這樣的工具都在總結了大量實(shí)踐后提供了優(yōu)化方案。 比如以下循環(huán) i = 0; while (a[i++] != 0) { ... } 應改為 loop: temp = i; /* 保存操作數的值 */ i = temp + 1; /* 遞增操作數 */ if (a[temp] == 0) /* 使用保存的值 */ goto no_loop; ... goto loop; no_loop: 或 loop: temp = a; /* 使用操作數的值 */ i = i + 1; /* 遞增操作數 */ if (temp == 0) goto no_loop; ... goto loop; no_loop: 如果循環(huán)后的 i 的值不相關(guān),最好將增量放在循環(huán)內。比如以下幾乎相同的循環(huán) i = 0; while (a != 0) { ++i; ... } 可以在沒(méi)有臨時(shí)變量的情況下執行: loop: if (a == 0) goto no_loop; i = i + 1; ... goto loop; no_loop: 優(yōu)化編譯器的開(kāi)發(fā)者們很清楚后增量會(huì )使代碼編寫(xiě)變得更復雜,盡管我們已盡力去識別這些模式,并盡量消除臨時(shí)變量,但總有一些情況使我們無(wú)法產(chǎn)生有效代碼,尤其是遇到比上述更復雜的循環(huán)條件時(shí)。通常,我們會(huì )將一個(gè)復雜的表達式分割成若干個(gè)更簡(jiǎn)單的表達式,就像上面的循環(huán)條件被分割成一個(gè)測試和一個(gè)增量那樣。 在 C++ 環(huán)境中,選擇前增量還是后增量的重要性更高。這是因為 operator++ 和 operator-- 都可以以前綴和后綴的形式重載。將運算符作為類(lèi)對象重載時(shí),雖然沒(méi)必要模仿基本類(lèi)型運算符的行為,但也應盡量接近。因此,對于那些可以直觀(guān)地對對象進(jìn)行遞增和遞減的類(lèi),例如迭代器,通常會(huì )有前綴(operator++() 和 operator--())和后綴形式(operator++(int) 和 operator--(int))。 為了模擬基本類(lèi)型的前綴 ++ 的行為,operator++() 可以修改對象并返回對修改后對象的引用。那么模擬基本類(lèi)型的后綴 ++ 的行為會(huì )怎樣?您還記得嗎?“后綴 ++ 運算符的結果是操作數的值。在得到結果后,操作數的值會(huì )遞增”。就像上面的非直線(xiàn)代碼一樣,operator++(int) 的實(shí)現者必須復制原始對象,修改原始對象,并按值返回副本。由于存在復制操作,因此 operator++(int) 的開(kāi)銷(xiāo)要高于 operator++()。 對于基本類(lèi)型,如果忽略 i++ 的結果,優(yōu)化器通?梢韵槐匾膹椭,但優(yōu)化器不能將對一個(gè)重載運算符的調用變?yōu)榱硪粋(gè)。如果您出于習慣編寫(xiě) i++ 而不是 ++i,您就會(huì )調用開(kāi)銷(xiāo)更大的增量運算符。 雖然我們一直在反對使用后增量,但不得不承認,后增量在有些情況下還是有用的。如果確實(shí)要給一個(gè)變量進(jìn)行后置增量操作,那就繼續吧。如果后增量操作和您期望的操作一致,可以使用后增量操作。但請注意,切勿為避免多寫(xiě)一行代碼來(lái)遞增變量,而使用后增量操作。 每當您在循環(huán)條件、if 條件、switch 表達式、?:- 表達式或函數調用參數中添加不必要的后增量時(shí),都會(huì )使編譯器不得不生成更大、更慢的代碼。這個(gè)清單是不是太長(cháng)了,記不?今天就開(kāi)始培養好的習慣吧!在使用后增量操作前,先問(wèn)問(wèn)自己能不能把增量操作作為下一條語(yǔ)句。 結語(yǔ) 當然,軟件開(kāi)發(fā)工作并不是只要求開(kāi)發(fā)人員去“將就”編譯器,他們與編譯器之間的相互協(xié)同是快速而高效地完成編程工作的基礎之一。此外,從編譯器的發(fā)展過(guò)程來(lái)看,它們不僅要跟隨技術(shù)和語(yǔ)言的演進(jìn)而迭代和創(chuàng )新,而且還要廣泛參考更多的開(kāi)發(fā)習慣,那些歷史更悠久、使用更廣泛的編譯器可以為開(kāi)發(fā)人員帶來(lái)更高的效率。 因此,在了解了如何編寫(xiě)利于一款優(yōu)秀編譯器優(yōu)化的代碼之后,用戶(hù)們的工作效率就可以事半功倍。本文中提到的這些原理和tips,也是IAR Systems這樣的公司長(cháng)時(shí)間總結的最優(yōu)實(shí)踐,而且都可以在該公司的Embedded Workbench中進(jìn)行驗證和探索,在其工具界面中可以查看代碼的執行時(shí)間和代碼尺寸,從而找到最佳解決方案。 ![]() 好的工具除了通用的代碼編譯優(yōu)化,還支持高度靈活的自定義優(yōu)化設置,如IAR Embedded Workbench包含針對運行效率和代碼體積的不同優(yōu)化等級,對于不同的應用需求,還可以設置從整個(gè)工程,到每個(gè)源代碼文件,甚至是每個(gè)函數的優(yōu)化等級,幫助工程師為自己的應用適配出最佳的優(yōu)化方案。希望此篇文章對于開(kāi)發(fā)人員更深度地了解程序優(yōu)化有所幫助。 關(guān)于更多嵌入式相關(guān)的知識,歡迎關(guān)注IAR Systems的官方微信公眾號。 |