優(yōu)化C代碼中的環(huán)路終止
循環(huán)是大多數(shù)程序中的常見(jiàn)結(jié)構(gòu)。由于大量的執(zhí)行時(shí)間通?;ㄙM(fèi)在循環(huán)中,因此值得關(guān)注時(shí)間關(guān)鍵循環(huán)。
如果不謹(jǐn)慎地編寫,環(huán)路終止條件可能會(huì)導(dǎo)致大量開(kāi)銷。在可能的情況下:
-
使用簡(jiǎn)單的終止條件。
-
寫入倒計(jì)時(shí)到零循環(huán)。
-
使用?
unsigned int
?類型的計(jì)數(shù)器。 -
測(cè)試與零的相等性。
單獨(dú)或組合遵循這些準(zhǔn)則中的任何或全部準(zhǔn)則可能會(huì)產(chǎn)生更好的代碼。
下表顯示了用于計(jì)算?n!
?的例程的兩個(gè)示例實(shí)現(xiàn),它們共同說(shuō)明了環(huán)路終止開(kāi)銷。第一個(gè)實(shí)現(xiàn)使用遞增循環(huán)計(jì)算 n!,而第二個(gè)例程使用遞減循環(huán)計(jì)算?n!
。
表7-1 遞增和遞減循環(huán)的C代碼
遞增循環(huán) | 遞減循環(huán) |
---|---|
|
|
下表顯示了?armclang -Os -S --target=armv8a-arm-none-eabi
?針對(duì)上述每個(gè)示例實(shí)現(xiàn)生成的機(jī)器代碼的相應(yīng)反匯編。
表 7-2 C 遞增和遞減循環(huán)的反匯編
遞增循環(huán) | 遞減循環(huán) |
---|---|
|
|
比較反匯編表明,遞增循環(huán)反匯編中的?ADD
?和?CMP
?指令對(duì)已替換為遞減循環(huán)反匯編中的單個(gè)?SUBS
?指令。由于?SUBS
?指令更新?tīng)顟B(tài)標(biāo)志(包括 Z 標(biāo)志),因此不需要顯式?CMP r1、r2
?指令。
除了在循環(huán)中保存指令外,變量?n
?不必在循環(huán)的生命周期內(nèi)可用,從而減少了必須維護(hù)的寄存器數(shù)量。這簡(jiǎn)化了寄存器分配。如果原始終止條件涉及函數(shù)調(diào)用,則更為重要。例如:
for (...; i < get_limit(); ...);
將循環(huán)計(jì)數(shù)器初始化為所需迭代次數(shù),然后遞減到零的技術(shù)也適用于?while
?和?do
?語(yǔ)句。
?
C 代碼中的循環(huán)展開(kāi)
循環(huán)是大多數(shù)程序中的常見(jiàn)結(jié)構(gòu)。由于大量的執(zhí)行時(shí)間通?;ㄙM(fèi)在循環(huán)中,因此值得關(guān)注時(shí)間關(guān)鍵循環(huán)。
可以展開(kāi)小循環(huán)以獲得更高的性能,但缺點(diǎn)是代碼大小增加。展開(kāi)循環(huán)時(shí),循環(huán)計(jì)數(shù)器需要更新的頻率較低,執(zhí)行的分支也較少。如果循環(huán)只迭代幾次,則可以完全展開(kāi),使循環(huán)開(kāi)銷完全消失。編譯器在 -O3 -Otime
?處自動(dòng)展開(kāi)循環(huán)。否則,任何展開(kāi)都必須在源代碼中完成。
注意
手動(dòng)展開(kāi)循環(huán)可能會(huì)阻礙編譯器自動(dòng)重新滾動(dòng)循環(huán)和其他循環(huán)優(yōu)化。
可以使用下表中所示的兩個(gè)示例例程來(lái)說(shuō)明循環(huán)展開(kāi)的優(yōu)缺點(diǎn)。這兩個(gè)例程都通過(guò)提取最低位并對(duì)其進(jìn)行計(jì)數(shù)來(lái)有效地測(cè)試單個(gè)位,然后將該位移出。
第一種實(shí)現(xiàn)使用循環(huán)來(lái)計(jì)算位數(shù)。第二個(gè)例程是第一個(gè)展開(kāi)四次的實(shí)現(xiàn),通過(guò)將?n
?的四個(gè)班次合并為一個(gè)班次來(lái)應(yīng)用優(yōu)化。
頻繁展開(kāi)提供了新的優(yōu)化機(jī)會(huì)。
表 7-3 滾動(dòng)和展開(kāi)位計(jì)數(shù)循環(huán)的 C 代碼
位計(jì)數(shù)循環(huán) | 展開(kāi)的位計(jì)數(shù)循環(huán) |
---|---|
|
|
下表顯示了編譯器為上述每個(gè)示例實(shí)現(xiàn)生成的機(jī)器代碼的相應(yīng)反匯編,其中每個(gè)實(shí)現(xiàn)的 C 代碼已使用?armclang -Os -S --target=armv8a-arm-none-eabi
?編譯。
表7-4 滾動(dòng)和展開(kāi)的位計(jì)數(shù)循環(huán)的反匯編
位計(jì)數(shù)循環(huán) | 展開(kāi)的位計(jì)數(shù)循環(huán) |
---|---|
|
|
位計(jì)數(shù)循環(huán)的展開(kāi)版本比原始版本更快,但代碼大小更大。
?
編譯器優(yōu)化和 volatile 關(guān)鍵字
較高的優(yōu)化級(jí)別可以揭示某些程序中的問(wèn)題,這些問(wèn)題在較低的優(yōu)化級(jí)別下并不明顯,例如,缺少易失性
限定符。
這可以通過(guò)多種方式表現(xiàn)出來(lái)。輪詢硬件時(shí),代碼可能會(huì)卡在循環(huán)中,多線程代碼可能會(huì)表現(xiàn)出奇怪的行為,或者優(yōu)化可能會(huì)導(dǎo)致刪除實(shí)現(xiàn)故意計(jì)時(shí)延遲的代碼。在這種情況下,可能需要將某些變量聲明為可變
變量。
將變量聲明為?volatile
?告訴編譯器,該變量可以在實(shí)現(xiàn)外部隨時(shí)修改,例如,由操作系統(tǒng)、另一個(gè)執(zhí)行線程(如中斷例程或信號(hào)處理程序)或硬件進(jìn)行修改。由于可變
限定變量的值可以隨時(shí)更改,因此每當(dāng)在代碼中引用該變量時(shí),都必須始終訪問(wèn)內(nèi)存中的實(shí)際變量。這意味著編譯器無(wú)法對(duì)變量執(zhí)行優(yōu)化,例如,將其值緩存在寄存器中以避免內(nèi)存訪問(wèn)。同樣,在實(shí)現(xiàn)睡眠或計(jì)時(shí)器延遲的上下文中使用時(shí),將變量聲明為可變
變量會(huì)告訴編譯器有特定類型的行為是有意的,并且此類代碼不得以刪除預(yù)期功能的方式進(jìn)行優(yōu)化。
相反,當(dāng)變量未聲明為可變
變量時(shí),編譯器可以假定其值不能以意外方式修改。因此,編譯器可以對(duì)變量執(zhí)行優(yōu)化。
下表中的兩個(gè)示例例程說(shuō)明了?volatile
?關(guān)鍵字的用法。這兩個(gè)例程都在循環(huán)中讀取緩沖區(qū),直到狀態(tài)標(biāo)志?buffer_full
?設(shè)置為 true。buffer_full
的狀態(tài)可以隨程序流異步更改。
例程的兩個(gè)版本僅在聲明buffer_full
的方式上有所不同。第一個(gè)例程版本不正確。請(qǐng)注意,變量?buffer_full
?在此版本中未限定為?volatile
。相比之下,例程的第二個(gè)版本顯示了相同的循環(huán),其中buffer_full
被正確地限定為易失性
。
表 7-5 非易失性和易失性緩沖器環(huán)路的 C 代碼
緩沖環(huán)路的非易失性版本 | 緩沖區(qū)循環(huán)的易失性版本 |
---|---|
|
|
下表顯示了編譯器為上述每個(gè)示例生成的機(jī)器代碼的相應(yīng)反匯編,其中每個(gè)實(shí)現(xiàn)的 C 代碼已使用?armclang -Os -S --target=armv8a-arm-none-eabi
?進(jìn)行編譯。
表7-6 非易失性和易失性緩沖器環(huán)路的反匯編
緩沖環(huán)路的非易失性版本 | 緩沖區(qū)循環(huán)的易失性版本 |
---|---|
|
|
在上表中緩沖環(huán)路的非易失性版本的反匯編中,語(yǔ)句 LDR r1?[r0]
?將?buffer_full
?的值加載到寄存器?r1
?外部標(biāo)記為?.LBB0_1
。由于?buffer_full
?未聲明為易失性
,因此編譯器假定其值不能在程序外部修改。編譯器已將?buffer_full
?的值讀入?r0
?中,因此在啟用優(yōu)化時(shí)會(huì)省略重新加載變量,因?yàn)槠渲禑o(wú)法更改。結(jié)果是標(biāo)記為 的無(wú)限循環(huán)。LBB0_1
。
相反,在反匯編緩沖區(qū)循環(huán)的易失性版本時(shí),編譯器假定?buffer_full
?的值可以在程序外部更改,并且不執(zhí)行任何優(yōu)化。因此,buffer_full
?的值被加載到寄存器?r2
?中,該寄存器位于標(biāo)記為?的循環(huán)中。LBB1_1
。因此,循環(huán)?.LBB1_1
在匯編代碼中正確實(shí)現(xiàn)。
為了避免由實(shí)現(xiàn)外部的程序狀態(tài)更改引起的優(yōu)化問(wèn)題,每當(dāng)變量的值可能以實(shí)現(xiàn)未知的方式意外更改時(shí),就必須將變量聲明為可變
變量。
在實(shí)踐中,每當(dāng)出現(xiàn)以下情況時(shí),都必須將變量聲明為可變
變量:
-
訪問(wèn)內(nèi)存映射的外圍設(shè)備。
-
在多個(gè)線程之間共享全局變量。
-
訪問(wèn)中斷例程或信號(hào)處理程序中的全局變量。
編譯器不會(huì)優(yōu)化已聲明為可變變量的變量。
?
C 和 C++ 中的堆棧使用
C 和 C++ 都大量使用堆棧。
例如,堆棧包含:
-
函數(shù)的返回地址。
-
必須保留的寄存器,由 ARM 64 位架構(gòu) (AAPCS64) 的 ARM 體系結(jié)構(gòu)過(guò)程調(diào)用標(biāo)準(zhǔn)確定,例如,在進(jìn)入子例程時(shí)保存寄存器內(nèi)容時(shí)。
-
局部變量,包括局部數(shù)組、結(jié)構(gòu)、聯(lián)合,在 C++ 中還包括類。
有些堆棧使用并不明顯,例如:
-
如果局部整數(shù)或浮點(diǎn)變量溢出(即未分配給寄存器),則會(huì)為其分配堆棧內(nèi)存。
-
結(jié)構(gòu)通常分配給堆棧。堆棧上保留了一個(gè)等效于?
sizeof(struct)
?的空間,該空間填充為 16 個(gè)字節(jié)的倍數(shù)。編譯器嘗試將結(jié)構(gòu)分配給寄存器。 -
如果在編譯時(shí)已知數(shù)組大小的大小,則編譯器會(huì)在堆棧上分配內(nèi)存。同樣,在堆棧上保留了一個(gè)等效于?
sizeof(struct)
?的空間,該空間填充為 16 個(gè)字節(jié)的倍數(shù)。注意
可變長(zhǎng)度數(shù)組的內(nèi)存在運(yùn)行時(shí)在堆上分配。 -
一些優(yōu)化可以引入新的臨時(shí)變量來(lái)保存中間結(jié)果。優(yōu)化包括:CSE 消除、實(shí)時(shí)范圍拆分和結(jié)構(gòu)拆分。編譯器嘗試將這些臨時(shí)變量分配給寄存器。如果沒(méi)有,它會(huì)將它們溢出到堆棧中。
-
通常,為僅支持 16 位編碼的 Thumb 指令的處理器編譯的代碼比 A64 代碼、ARM 代碼和為支持 32 位編碼的 Thumb 指令的處理器編譯的代碼更多地使用堆棧。這是因?yàn)?16 位編碼的 Thumb 指令只有 8 個(gè)寄存器可供分配,而 ARM 代碼和 32 位編碼的 Thumb 指令則有 14 個(gè)寄存器。
-
AAPCS64要求通過(guò)堆棧而不是寄存器傳遞某些函數(shù)參數(shù),具體取決于它們的類型、大小和順序。
估算堆棧使用情況的方法
堆棧使用情況很難估計(jì),因?yàn)樗蕾囉诖a,并且根據(jù)程序在執(zhí)行時(shí)采用的代碼路徑,在運(yùn)行之間可能會(huì)有所不同。但是,可以使用以下方法手動(dòng)估計(jì)堆棧利用率的程度:
-
使用 -
-callgraph
?鏈接以生成靜態(tài)調(diào)用圖。這顯示了有關(guān)所有功能的信息,包括堆棧使用情況。這將使用?
.debug_frame
?部分中的 DWARF 幀信息。使用?-g
?選項(xiàng)進(jìn)行編譯以生成必要的 DWARF 信息。 -
使用 --info=stack 或?
--info=summarystack
?鏈接以列出所有全局符號(hào)的堆棧使用情況。 -
使用調(diào)試器在堆棧中的最后一個(gè)可用位置設(shè)置觀察點(diǎn),并查看是否命中了觀察點(diǎn)。
-
使用調(diào)試器,然后:
-
在內(nèi)存中為比預(yù)期需要的堆棧大得多的堆棧分配空間。
-
用已知值的副本填充堆棧空間,例如?
0xDEADDEAD
。 -
運(yùn)行應(yīng)用程序或應(yīng)用程序的固定部分。目標(biāo)是在測(cè)試運(yùn)行中使用盡可能多的堆??臻g。例如,嘗試執(zhí)行最深嵌套的函數(shù)調(diào)用和靜態(tài)分析找到的最壞情況路徑。嘗試在適當(dāng)?shù)奈恢蒙芍袛?,以便將它們包含在堆棧跟蹤中?/span>
-
應(yīng)用程序完成執(zhí)行后,檢查內(nèi)存的堆??臻g,查看有多少已知值已被覆蓋。該空間在已使用部分中有垃圾,其余部分有已知值。
-
計(jì)算垃圾值的數(shù)量,然后乘以?
sizeof(value),
以給出它們的大?。ㄒ宰止?jié)為單位)。
計(jì)算結(jié)果顯示了堆棧大小是如何增長(zhǎng)的(以字節(jié)為單位)。
-
-
使用固定虛擬平臺(tái) (FVP),并使用映射文件定義一個(gè)內(nèi)存區(qū)域,不允許在內(nèi)存中堆棧的正下方進(jìn)行訪問(wèn)。如果堆棧溢出到禁止區(qū)域,則會(huì)發(fā)生數(shù)據(jù)中止,調(diào)試器可能會(huì)捕獲數(shù)據(jù)中止。
減少堆棧使用的方法
通常,可以通過(guò)以下方式降低程序的堆棧要求:
-
編寫只需要少量變量的小函數(shù)。
-
避免使用大型局部結(jié)構(gòu)或數(shù)組。
-
例如,通過(guò)使用替代算法來(lái)避免遞歸。
-
最小化函數(shù)中每個(gè)點(diǎn)在任何給定時(shí)間使用的變量數(shù)。
-
使用 C 塊作用域并僅在需要的地方聲明變量,因此與不同作用域使用的內(nèi)存重疊。
C 塊作用域的使用涉及僅在需要的地方聲明變量。這通過(guò)重疊不同作用域所需的內(nèi)存來(lái)最大程度地減少堆棧的使用。
最小化函數(shù)參數(shù)傳遞開(kāi)銷的方法
有多種方法可以最大程度地減少將參數(shù)傳遞給函數(shù)的開(kāi)銷。
例如:
- 在 AArch64 狀態(tài)下,可以有效地傳遞 8 個(gè)整數(shù)參數(shù)和 8 個(gè)浮點(diǎn)參數(shù)(總共 16 個(gè))。在 AArch32 狀態(tài)下,如果每個(gè)參數(shù)的大小不超過(guò)一個(gè)字,則確保函數(shù)采用四個(gè)或更少的參數(shù)。在 C++ 中,確保非靜態(tài)成員函數(shù)采用的參數(shù)不超過(guò)一個(gè)參數(shù),因?yàn)橥ǔT?
R0
?中傳遞隱式?this
?指針參數(shù)。 - 如果函數(shù)需要超過(guò)參數(shù)的有效限制,請(qǐng)確保函數(shù)執(zhí)行大量工作,以便超過(guò)傳遞堆疊參數(shù)的成本。
- 將相關(guān)參數(shù)放在結(jié)構(gòu)中,并在任何函數(shù)調(diào)用中傳遞指向該結(jié)構(gòu)的指針。這減少了參數(shù)的數(shù)量并提高了可讀性。
- 對(duì)于 32 位體系結(jié)構(gòu),應(yīng)盡量減少 long?
long
?參數(shù)的數(shù)量,因?yàn)檫@些參數(shù)需要兩個(gè)參數(shù)字,這兩個(gè)參數(shù)字必須在偶數(shù)寄存器索引上對(duì)齊。 - 對(duì)于 32 位體系結(jié)構(gòu),在使用軟件浮點(diǎn)時(shí),請(qǐng)盡量減少
雙精度
參數(shù)的數(shù)量。
?文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-779239.html
C 代碼中的整數(shù)除以零錯(cuò)誤
對(duì)于不支持?SDIV
?除法指令的目標(biāo),可以使用相應(yīng)的 C 庫(kù)輔助函數(shù) __aeabi_idiv0() 和?__rt_raise(
)
?捕獲和識(shí)別整數(shù)除以零錯(cuò)誤
關(guān)于使用 __aeabi_idiv0() 捕獲整數(shù)除以零錯(cuò)誤
您可以使用 C 庫(kù)輔助函數(shù)?__aeabi_idiv0()
?捕獲整數(shù)除以零錯(cuò)誤,以便除以零返回一些標(biāo)準(zhǔn)結(jié)果,例如零。
整數(shù)除法是通過(guò) C 庫(kù)輔助函數(shù) __aeabi_idiv() 和?__aeabi_uidiv()
?在代碼中實(shí)現(xiàn)的。這兩個(gè)函數(shù)都檢查除以零。
當(dāng)檢測(cè)到整數(shù)除以零時(shí),將創(chuàng)建?__aeabi_idiv0()
?的分支。因此,要將除法捕獲為零,只需在?__aeabi_idiv0()
?上放置一個(gè)斷點(diǎn)。
該庫(kù)提供了?__aeabi_idiv0()
?的兩種實(shí)現(xiàn)。默認(rèn)值不執(zhí)行任何操作,因此如果檢測(cè)到除以零,則除法函數(shù)返回零。但是,如果使用信號(hào)處理,則會(huì)選擇調(diào)用?__rt_raise(SIGFPE, DIVBYZERO)
?的替代實(shí)現(xiàn)。
如果您提供自己的?__aeabi_idiv0()
?版本,則除法函數(shù)將調(diào)用此函數(shù)。__aeabi_idiv0()
?的函數(shù)原型為:
int __aeabi_idiv0(void);
如果?__aeabi_idiv0()
?返回一個(gè)值,則該值用作除法函數(shù)返回的商。
關(guān)于使用 __rt_raise() 捕獲整數(shù)除以零錯(cuò)誤
默認(rèn)情況下,整數(shù)除以零返回零。如果要截獲除以零,可以重新實(shí)現(xiàn) C 庫(kù)輔助函數(shù)?__rt_raise()。
__rt_raise()
?的函數(shù)原型為:
void __rt_raise(int signal, int type);
如果重新實(shí)現(xiàn)?__rt_raise(),
則庫(kù)會(huì)自動(dòng)提供?__aeabi_idiv0(
) 的信號(hào)處理庫(kù)版本,該版本調(diào)用?__rt_raise(),
則該庫(kù)版本的?__aeabi_idiv0()
?將包含在最終映像中。
在這種情況下,當(dāng)發(fā)生除以零錯(cuò)誤時(shí),__aeabi_idiv0()
?調(diào)用?__rt_raise(SIGFPE, DIVBYZERO)。
因此,如果重新實(shí)現(xiàn)?__rt_raise(),
則必須選中?(signal == SIGFPE) & (type == DIVBYZERO)
?以確定是否發(fā)生了除以零的情況。
識(shí)別 C 代碼中的整數(shù)除以零錯(cuò)誤
進(jìn)入?__aeabi_idiv0(
) 時(shí),鏈路寄存器?LR
?包含應(yīng)用程序代碼中調(diào)用?__aeabi_uidiv()
?除法例程后的指令地址。
通過(guò)在調(diào)試器中查找?LR
?給出的地址處的 C 代碼行,可以識(shí)別源代碼中的違規(guī)行。文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-779239.html
?
到了這里,關(guān)于Keil5,ARM編譯器 軟件優(yōu)化注意事項(xiàng)的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!