《電子技術(shù)應(yīng)用》
您所在的位置:首頁(yè) > 嵌入式技術(shù) > 解決方案 > 如何編寫(xiě)有利于編譯器優(yōu)化的代碼

如何編寫(xiě)有利于編譯器優(yōu)化的代碼

2021-11-09
來(lái)源:IAR Systems
關(guān)鍵詞: 芯片 Flash RAM

  在嵌入式開(kāi)發(fā)中,,代碼的體積和運(yùn)行效率非常重要,代碼體積往往和芯片FLASH,、RAM容量對(duì)應(yīng),,程序的運(yùn)行效率也要求在相應(yīng)能力的處理器上運(yùn)行。在大多數(shù)情況下,,成熟的開(kāi)發(fā)人員都希望降低代碼體積,、提高代碼運(yùn)行效率,然而具體該怎么做呢,?本篇文章將以國(guó)際知名編譯器廠商IAR Systems的編譯器為例,,來(lái)解答開(kāi)發(fā)人員在實(shí)際工作中常常遇到的問(wèn)題,工程師朋友們可以在IAR編譯器上進(jìn)行實(shí)踐驗(yàn)證,。

  對(duì)于嵌入式系統(tǒng),,最終代碼的體積和效率取決于由編譯器生成的可執(zhí)行代碼,而非開(kāi)發(fā)人員編寫(xiě)的源代碼,;但是源代碼的優(yōu)化,,可以幫助編譯器生成更加優(yōu)質(zhì)的可執(zhí)行代碼。因此,,開(kāi)發(fā)人員不僅要從整體效率等因素上去構(gòu)思源代碼體系,,也要高度關(guān)注編譯器的性能和編譯優(yōu)化的便捷性。

  有優(yōu)化功能的編譯器可生成既小又快的可執(zhí)行代碼,,編譯器是通過(guò)對(duì)源代碼的重復(fù)轉(zhuǎn)換來(lái)實(shí)現(xiàn)優(yōu)化,。通常,編譯器優(yōu)化會(huì)遵循完善的數(shù)學(xué)或邏輯理論基礎(chǔ),。但是某些編譯優(yōu)化則是通過(guò)啟發(fā)式的方法,,經(jīng)驗(yàn)表明,,一些代碼轉(zhuǎn)換往往會(huì)產(chǎn)生更好的代碼,或者開(kāi)拓出進(jìn)一步編譯優(yōu)化的空間,。

  編譯優(yōu)化只有少數(shù)情況依賴于編譯器的黑科技,,大多數(shù)時(shí)候編寫(xiě)源代碼的方式?jīng)Q定了程序是否可以被編譯器優(yōu)化。在某些情況下,,即使對(duì)源代碼做微小改動(dòng)也會(huì)對(duì)編譯器生成的代碼效率產(chǎn)生重大影響,。

  本文將講述在編寫(xiě)代碼時(shí)需要注意的事項(xiàng),但我們首先應(yīng)明確一點(diǎn),,我們沒(méi)有必要盡量減少代碼量,,因?yàn)榧词乖谝粋€(gè)表達(dá)式中使用 ?:- 表達(dá)式、后增量和逗號(hào)表達(dá)式來(lái)消除副作用,,也不會(huì)使編譯器產(chǎn)生更有效的代碼,。這只會(huì)使你的源代碼變得晦澀難懂,難以維護(hù),。例如在一個(gè)復(fù)雜的表達(dá)式中間加入一個(gè)后增量或賦值,,則在讀代碼的時(shí)候很容易被忽略。請(qǐng)盡量用一種易于閱讀的風(fēng)格來(lái)編寫(xiě)代碼,。

  循環(huán)

  下面看似簡(jiǎn)單的循環(huán)會(huì)報(bào)錯(cuò)嗎,?

  for (i = 0; i != n; ++i)

  {

  a[i] = b[i];

  }

  雖然不會(huì)報(bào)錯(cuò),但其中有幾點(diǎn)會(huì)影響到編譯器生成的代碼效率,。

  例如,,索引變量的類型應(yīng)與指針相匹配。

  像 a[i] 這樣的數(shù)組表達(dá)式實(shí)際上是 *(&a[0]+i*sizeof(a[0]),,或者通俗地說(shuō):將第 i個(gè)元素的偏移量加到 a 的第一個(gè)元素的指針上,。對(duì)于指針運(yùn)算, 索引表達(dá)式的類型最好與指針?biāo)赶虻念愋鸵恢拢╛_far 指針除外,,因?yàn)槠渲羔標(biāo)赶虻念愋秃退饕磉_(dá)式的類型不同),。如果索引表達(dá)式的類型與指針?biāo)赶虻念愋筒黄ヅ?,那么在把它與指針相加之前,,必須將它強(qiáng)制轉(zhuǎn)換為正確的類型。

  如果在應(yīng)用中,,堆??臻g資源(堆棧一般放在RAM中)比代碼尺寸資源(代碼一般放在ROM或者Flash中)更寶貴,則可以為索引變量選擇一個(gè)更小的類型來(lái)減少堆??臻g的使用,,但這往往會(huì)犧牲代碼尺寸和執(zhí)行時(shí)間(代碼尺寸變大,執(zhí)行時(shí)間變慢),。不僅如此,,這種轉(zhuǎn)換也會(huì)妨礙循環(huán)代碼的優(yōu)化,。

  除上述問(wèn)題外,我們也要關(guān)注循環(huán)條件,,因?yàn)橹挥性谶M(jìn)入循環(huán)之前可以計(jì)算出迭代次數(shù)的情況下,,才可以進(jìn)行循環(huán)優(yōu)化。然而,,這項(xiàng)計(jì)算工作非常復(fù)雜,,并非用最終值減去初始值并除以增量那么簡(jiǎn)單。例如,,如果 i 是一個(gè)無(wú)符號(hào)字符,,n 是一個(gè)整數(shù),而 n 的值是 1000,,那么會(huì)發(fā)生什么情況,?答案是變量 i 在達(dá)到 1000 之前就會(huì)溢出。

  雖然程序員肯定不想要一個(gè)無(wú)限循環(huán),,重復(fù)地將 256 個(gè)元素從 b 復(fù)制到 a,,但是編譯器無(wú)法了解程序員的意圖。它必須假設(shè)最壞的情況,,并且不能應(yīng)用需要在進(jìn)入循環(huán)之前提供行程數(shù)的優(yōu)化,。此外,如果最終值是一個(gè)變量,,您還應(yīng)該避免在循環(huán)條件中使用關(guān)系運(yùn)算符 <= 和 >=,。如果循環(huán)條件是 i <= n,那么 n 有可能是該類型中可表示的最高值,,因此編譯器必須假定這是一個(gè)潛在的無(wú)限循環(huán),。

  別名

  通常,我們不建議使用全局變量,。這是因?yàn)槟稍诔绦虻娜魏蔚胤叫薷娜肿兞?,并且程序?huì)因全局變量的值而變化。這就會(huì)形成復(fù)雜的依賴關(guān)系,,使人很難理解程序,,也很難確定改變?nèi)肿兞康闹禃?huì)對(duì)程序產(chǎn)生怎樣的影響。從優(yōu)化器的角度來(lái)看,,這種情況更糟糕,,因?yàn)橥ㄟ^(guò)指針的存儲(chǔ)就可以改變?nèi)我馊肿兞康闹怠H绻芡ㄟ^(guò)多種方式訪問(wèn)一個(gè)變量,,這種情況就會(huì)被稱為別名,,而別名使代碼更難優(yōu)化。

  char *buf

  void clear_buf()

  {

  int i;

  for (i = 0; i < 128; ++i)

  {

  buf[i] = 0;

  }

  }

  盡管程序員知道向 buf 所指向的緩存區(qū)進(jìn)行寫(xiě)操作不會(huì)改變這個(gè)buf變量本身,但編譯器還是不得不做最壞的打算,,在循環(huán)的每一次迭代中從內(nèi)存中重新加載 buf,。

  如果將緩存區(qū)的地址作為參數(shù)傳遞,而不是使用全局變量,,則可以消除別名:

  void clear_buf(char *buf)

  {

  int i;

  for (i = 0; i < 128; ++i)

  {

  buf[i] = 0;

  }

  }

  使用這個(gè)解決方案后,,指針 buf 就不會(huì)被通過(guò)指針的存儲(chǔ)影響。如此一來(lái),,指針 buf 在循環(huán)中就可以保持不變,,其值只需在循環(huán)前加載一次即可,而不是在每次迭代時(shí)都要重新加載,。

  然而,,如果需要在不共享調(diào)用者/被調(diào)用者關(guān)系的代碼段之間傳遞信息,則直接使用全局變量即可,。但是,,對(duì)于計(jì)算密集型任務(wù),尤其是涉及指針操作時(shí),,最好使用自動(dòng)變量,。

  盡量不用后增量和后減量

  在下文中,關(guān)于后增量的所有內(nèi)容也適用于后減量,。C 語(yǔ)言中關(guān)于后增量語(yǔ)義的標(biāo)準(zhǔn)文本指出:“后綴 ++ 運(yùn)算符的結(jié)果是操作數(shù)的值,。在得到結(jié)果后,操作數(shù)的值會(huì)遞增”,。雖然微控制器普遍擁有可在加載或存儲(chǔ)操作后增加指針的尋址模式,,但其中只有很少能以同樣的效率處理其他類型的后增量。為符合標(biāo)準(zhǔn),,編譯器必須在執(zhí)行增量之前將操作數(shù)復(fù)制到一個(gè)臨時(shí)變量,。對(duì)于直線代碼來(lái)說(shuō),可以從表達(dá)式中取出增量,,然后放在表達(dá)式之后,。比如以下表達(dá)式:

  foo = a[i++];

  可以改為

  foo = a[i];

  i = i + 1;

  但如果后增量屬于 while 循環(huán)中的條件,又會(huì)發(fā)生什么,?由于在條件后面沒(méi)有可以插入增量的地方,,因此必須在測(cè)試前添加增量。對(duì)于這些常見(jiàn)但是又與生成可執(zhí)行代碼效率密切相關(guān)的設(shè)計(jì),,諸如IAR Systems的Embedded Workbench這樣的工具都在總結(jié)了大量實(shí)踐后提供了優(yōu)化方案,。

  比如以下循環(huán)

  i = 0;

  while (a[i++] != 0)

  {

  …

  }

  應(yīng)改為

  loop:

  temp = i; /* 保存操作數(shù)的值 */

  i = temp + 1; /* 遞增操作數(shù) */

  if (a[temp] == 0) /* 使用保存的值 */

  goto no_loop;

  …

  goto loop;

  no_loop:

  或

  loop:

  temp = a[i]; /* 使用操作數(shù)的值 */

  i = i + 1; /* 遞增操作數(shù) */

  if (temp == 0)

  goto no_loop;

  …

  goto loop;

  no_loop:

  如果循環(huán)后的 i 的值不相關(guān),,最好將增量放在循環(huán)內(nèi),。比如以下幾乎相同的循環(huán)

  i = 0;

  while (a[i] != 0)

  {

  ++i;

  …

  }

  可以在沒(méi)有臨時(shí)變量的情況下執(zhí)行:

  loop:

  if (a[i] == 0)

  goto no_loop;

  i = i + 1;

  …

  goto loop;

  no_loop:

  優(yōu)化編譯器的開(kāi)發(fā)者們很清楚后增量會(huì)使代碼編寫(xiě)變得更復(fù)雜,盡管我們已盡力去識(shí)別這些模式,,并盡量消除臨時(shí)變量,,但總有一些情況使我們無(wú)法產(chǎn)生有效代碼,,尤其是遇到比上述更復(fù)雜的循環(huán)條件時(shí)。通常,,我們會(huì)將一個(gè)復(fù)雜的表達(dá)式分割成若干個(gè)更簡(jiǎn)單的表達(dá)式,,就像上面的循環(huán)條件被分割成一個(gè)測(cè)試和一個(gè)增量那樣。

  在 C++ 環(huán)境中,,選擇前增量還是后增量的重要性更高,。這是因?yàn)?operator++ 和 operator-- 都可以以前綴和后綴的形式重載。將運(yùn)算符作為類對(duì)象重載時(shí),,雖然沒(méi)必要模仿基本類型運(yùn)算符的行為,,但也應(yīng)盡量接近。因此,,對(duì)于那些可以直觀地對(duì)對(duì)象進(jìn)行遞增和遞減的類,,例如迭代器,通常會(huì)有前綴(operator++() 和 operator--())和后綴形式(operator++(int) 和 operator--(int)),。

  為了模擬基本類型的前綴 ++ 的行為,,operator++() 可以修改對(duì)象并返回對(duì)修改后對(duì)象的引用。那么模擬基本類型的后綴 ++ 的行為會(huì)怎樣,?您還記得嗎,?“后綴 ++ 運(yùn)算符的結(jié)果是操作數(shù)的值。在得到結(jié)果后,,操作數(shù)的值會(huì)遞增”,。就像上面的非直線代碼一樣,operator++(int) 的實(shí)現(xiàn)者必須復(fù)制原始對(duì)象,,修改原始對(duì)象,,并按值返回副本。由于存在復(fù)制操作,,因此 operator++(int) 的開(kāi)銷要高于 operator++(),。

  對(duì)于基本類型,如果忽略 i++ 的結(jié)果,,優(yōu)化器通??梢韵槐匾膹?fù)制,但優(yōu)化器不能將對(duì)一個(gè)重載運(yùn)算符的調(diào)用變?yōu)榱硪粋€(gè),。如果您出于習(xí)慣編寫(xiě) i++ 而不是 ++i,,您就會(huì)調(diào)用開(kāi)銷更大的增量運(yùn)算符。

  雖然我們一直在反對(duì)使用后增量,,但不得不承認(rèn),,后增量在有些情況下還是有用的。如果確實(shí)要給一個(gè)變量進(jìn)行后置增量操作,那就繼續(xù)吧,。如果后增量操作和您期望的操作一致,,可以使用后增量操作。但請(qǐng)注意,,切勿為避免多寫(xiě)一行代碼來(lái)遞增變量,,而使用后增量操作。

  每當(dāng)您在循環(huán)條件,、if 條件,、switch 表達(dá)式、,?:- 表達(dá)式或函數(shù)調(diào)用參數(shù)中添加不必要的后增量時(shí),,都會(huì)使編譯器不得不生成更大、更慢的代碼,。這個(gè)清單是不是太長(zhǎng)了,,記不住,?今天就開(kāi)始培養(yǎng)好的習(xí)慣吧,!在使用后增量操作前,先問(wèn)問(wèn)自己能不能把增量操作作為下一條語(yǔ)句,。

  結(jié)語(yǔ)

  當(dāng)然,,軟件開(kāi)發(fā)工作并不是只要求開(kāi)發(fā)人員去“將就”編譯器,他們與編譯器之間的相互協(xié)同是快速而高效地完成編程工作的基礎(chǔ)之一,。此外,,從編譯器的發(fā)展過(guò)程來(lái)看,它們不僅要跟隨技術(shù)和語(yǔ)言的演進(jìn)而迭代和創(chuàng)新,,而且還要廣泛參考更多的開(kāi)發(fā)習(xí)慣,,那些歷史更悠久、使用更廣泛的編譯器可以為開(kāi)發(fā)人員帶來(lái)更高的效率,。

  因此,,在了解了如何編寫(xiě)利于一款優(yōu)秀編譯器優(yōu)化的代碼之后,用戶們的工作效率就可以事半功倍,。本文中提到的這些原理和tips,,也是IAR Systems這樣的公司長(zhǎng)時(shí)間總結(jié)的最優(yōu)實(shí)踐,而且都可以在該公司的Embedded Workbench中進(jìn)行驗(yàn)證和探索,,在其工具界面中可以查看代碼的執(zhí)行時(shí)間和代碼尺寸,,從而找到最佳解決方案。

  

1636430355442318.jpg

  好的工具除了通用的代碼編譯優(yōu)化,,還支持高度靈活的自定義優(yōu)化設(shè)置,,如IAR Embedded Workbench包含針對(duì)運(yùn)行效率和代碼體積的不同優(yōu)化等級(jí),,對(duì)于不同的應(yīng)用需求,還可以設(shè)置從整個(gè)工程,,到每個(gè)源代碼文件,甚至是每個(gè)函數(shù)的優(yōu)化等級(jí),,幫助工程師為自己的應(yīng)用適配出最佳的優(yōu)化方案,。希望此篇文章對(duì)于開(kāi)發(fā)人員更深度地了解程序優(yōu)化有所幫助。




圖片.jpg


本站內(nèi)容除特別聲明的原創(chuàng)文章之外,,轉(zhuǎn)載內(nèi)容只為傳遞更多信息,,并不代表本網(wǎng)站贊同其觀點(diǎn)。轉(zhuǎn)載的所有的文章,、圖片,、音/視頻文件等資料的版權(quán)歸版權(quán)所有權(quán)人所有。本站采用的非本站原創(chuàng)文章及圖片等內(nèi)容無(wú)法一一聯(lián)系確認(rèn)版權(quán)者,。如涉及作品內(nèi)容,、版權(quán)和其它問(wèn)題,請(qǐng)及時(shí)通過(guò)電子郵件或電話通知我們,,以便迅速采取適當(dāng)措施,,避免給雙方造成不必要的經(jīng)濟(jì)損失。聯(lián)系電話:010-82306118,;郵箱:[email protected],。