通過(guò)一定的方法來(lái)編寫(xiě)C程序,可以幫助C編譯器生成執行速度更快的ARM代碼。下面就是一些與性能相關(guān)的關(guān)鍵點(diǎn): 1.對局部變量、函數參數和返回值要使用signed和unsigned int類(lèi)型。這樣可以避免類(lèi)型轉換,而且可高效地使用ARM的32位數據操作指令。 2.最高效的循環(huán)體形式是減計數到零(counts down to zero)的do-while循環(huán)。 3.展開(kāi)重要的循環(huán)來(lái)減少循環(huán)的開(kāi)銷(xiāo)。 4.不要依賴(lài)編譯器來(lái)優(yōu)化掉重復的存儲器訪(fǎng)問(wèn)。指針別名會(huì )阻止編譯器的這種優(yōu)化。 5.盡可能把函數參數的個(gè)數限制在4個(gè)以?xún)。如果函數參數都存放在寄存器內,那么函數調用就會(huì )快得多。 6.按元素尺寸從小到大排列的方法來(lái)安排結構體,特別是在thumb模式下編譯。 7.不要使用位域,可以用掩碼和邏輯操作來(lái)替代。 8.避免除法,可以用倒數的乘法來(lái)替代。 9.避免邊界不對齊的數據。如果數據有可能邊界不對齊,那么就要使用char *指針類(lèi)型來(lái)訪(fǎng)問(wèn)。 10.在C編譯器中使用內嵌匯編可以利用到C編譯器本來(lái)不支持的指令或優(yōu)化。 一、 數據類(lèi)型使用上的優(yōu)化 1.局部變量 一個(gè)char類(lèi)型的數據比int類(lèi)型的數據占用更小的寄存器空間或者更小的ARM堆?臻g。這兩種設想對于A(yíng)RM來(lái)說(shuō),都是錯誤的。所有的ARM寄存器都是32位的,所有的堆棧入口至少是32位的。當我們執行i++,要利用當i=255后,i++=0這個(gè)條件時(shí),可以把它定義為char類(lèi)型。 2.函數參數 盡管寬和窄的函數調用規則各有其優(yōu)點(diǎn),但char或short類(lèi)型的函數參數和返回值都會(huì )產(chǎn)生額外的開(kāi)銷(xiāo),導致性能的下降,并增加了代碼尺寸。所以,即使是傳輸一個(gè)8位的數據,函數參數和返回值使用int類(lèi)型也會(huì )更有效。 總結: 1)對于存放在寄存器中的局部變量,除了8位或16位的算術(shù)模運算外,盡量不要使用char和short類(lèi)型,而要使用有符號或無(wú)符號int類(lèi)型。除法運算時(shí)使用無(wú)符號數執行速度更快。 2)對于存放在主存儲器中的數組和全局變量,在滿(mǎn)足數據大小的前提下,應盡可能使用小尺寸的數據類(lèi)型,這樣可以節省存儲空間。ARMv4體系結構可以有效地裝載和存儲所有寬度的數據,并可以使用遞增數組指針來(lái)有效地訪(fǎng)問(wèn)數組。對于short類(lèi)型數組,要避免使用數組基地址的偏移量,因為L(cháng)DRH指令不支持偏移尋址。 3)通過(guò)讀取數組或全局變量并賦給不同類(lèi)型的局部變量時(shí),或者把局部變量寫(xiě)入不同類(lèi)型的數組或者全局變量時(shí),要進(jìn)行顯式數據類(lèi)型轉換。這種轉換使編譯器可以明確、快速地處理,把存儲器中數據寬度比較窄的數據類(lèi)型擴展,并賦給寄存器中較寬的類(lèi)型。 4)由于隱式或者顯式的數據類(lèi)型轉換通常會(huì )有額外的指令周期開(kāi)銷(xiāo),所以在表達式中應盡量避免使用。Load和store指令一般不會(huì )產(chǎn)生額外的轉換開(kāi)銷(xiāo),因為load和store指令是自動(dòng)完成數據類(lèi)型轉換的。 5)對于函數參數和返回值應盡量避免使用char和short類(lèi)型。即使參數范圍比較小,也應該使用int類(lèi)型,以防止編譯器做不必要的類(lèi)型轉換。 二、C循環(huán)結構 在A(yíng)RM上,一個(gè)循環(huán)其實(shí)只要2條指令就足夠了: 一條減法指令,進(jìn)行循環(huán)減法計數,同時(shí)設置結果的條件標志; 一條條件分支指令。 這里的關(guān)鍵是,循環(huán)的終止條件應為減計數到零,而不是計數增加到某個(gè)特定的限制值。由于減計數結構已存儲在條件標志里,與零比較的指令就可以省略了。由于不用i作為數組的下標索引,采用減計數就沒(méi)有任何問(wèn)題了。 總而言之,無(wú)論對于有符號的循環(huán)計數值,都應使用i!=0作為循環(huán)的結束條件。對有符號數i,這比使用條件i>0少了一條指令。 總結: 1) 使用減計數到零的循環(huán)結構,這樣編譯器就不需要分配一個(gè)寄存器來(lái)保存循環(huán)終止值,而且與0比較的指令也可以省略。 2) 使用無(wú)符號的循環(huán)計數值,循環(huán)繼續的條件為i!=0而不是i>0,這樣可以保證循環(huán)開(kāi)銷(xiāo)只有兩條指令。 3) 如果事先知道循環(huán)體至少會(huì )執行一次,那么使用do-while循環(huán)要比f(wàn)or循環(huán)要好,這樣可以使編譯器省去檢查循環(huán)計數值是否為零的步驟。 4) 展開(kāi)重要的循環(huán)體可降低循環(huán)開(kāi)銷(xiāo),但不要過(guò)度展開(kāi),如果循環(huán)的開(kāi)銷(xiāo)對整個(gè)程序來(lái)說(shuō)占的比例很小,那么循環(huán)展開(kāi)反而會(huì )增加代碼量并降低cache的性能。 5) 盡量使數組的大小是4或8的倍數,這樣可以容易的以2,4,8次等多種選擇展開(kāi)循環(huán),而不需要擔心剩余數組元素的問(wèn)題。 三、寄存器分配 高效的寄存器分配:應該盡量限制函數內部循環(huán)所用局部變量的數目,最多不超過(guò)12個(gè),這樣,編譯器就可以把這些變量都分配給ARM寄存器。 四、函數調用 4寄存器規則:帶有4個(gè)或者更少參數的函數,要比多于4個(gè)參數的函數執行效率高得多。對帶有少于4個(gè)參數的函數來(lái)說(shuō),編譯器可以用寄存器傳遞所有的參數;而對于多于4個(gè)參數的函數,函數調用者和被調用者必須通過(guò)訪(fǎng)問(wèn)堆棧來(lái)傳遞一些參數。 如果函數體積很小,只用到很少的寄存器,那么還有一些其他的方法來(lái)減少函數調用的開(kāi)銷(xiāo)?梢园颜{用函數和被調用函數放在同一個(gè)C文件中,這樣編譯器就知道了被調用函數生成的代碼,并以此對調用函數進(jìn)行一些優(yōu)化。 總結: 1) 盡量限制函數的參數,不要超過(guò)4個(gè),這樣函數調用的效率會(huì )更高。也可以將幾個(gè)相關(guān)的參數組織在一個(gè)結構體中,用傳遞結構體指針來(lái)代替多個(gè)參數。 2) 把比較小的被調用函數和調用函數放在同一個(gè)源文件中,并且要先定義,后調用,編譯器就可以?xún)?yōu)化函數調用或者內聯(lián)較小的函數。 3) 對性能影響較大的重要函數可使用關(guān)鍵字_inline進(jìn)行內聯(lián)。 五、指針別名 定義:當2個(gè)指針指向同一個(gè)地址對象時(shí),這2個(gè)指針被稱(chēng)作該對象的別名(alias)。如果對其中一個(gè)指針進(jìn)行寫(xiě)入,就會(huì )影響從另一個(gè)指針的讀出。在一個(gè)函數中,編譯器通常不知道哪一個(gè)指針是別名,哪一個(gè)不是;或哪一個(gè)指針有別名,哪一個(gè)沒(méi)有。 避免指針別名: 1) 不要依賴(lài)編譯器來(lái)消除包含存儲器訪(fǎng)問(wèn)的公共子表達式,而應建立一個(gè)新的局部變量來(lái)保存這個(gè)表達式的值,這樣可以保證只對這個(gè)表達式求一次值; 2) 避免使用局部變量的地址,否則對這個(gè)變量的訪(fǎng)問(wèn)效率會(huì )比較低。 六、結構體安排 在A(yíng)RM上使用結構體有2個(gè)問(wèn)題需要考慮:結構體地址邊界對齊和結構體總的大小。 獲得高效結構體的原則: 1) 把所有8位大小的元素安排在結構體的前面; 2) 以此安排16位、32位和64位的元素; 3) 把所有數組和比較大的元素安排在結構體最后; 4) 對于一條指令,如果結構體太大而不能訪(fǎng)問(wèn)所有的元素,那么把元素組織到一個(gè)子結構體中。編譯器可以維持單獨的子結構體的指針。 總結: 結構體元素要按照元素的大小來(lái)排列,以最小的元素放在開(kāi)始,最大的元素安排在最后;避免使用很大的結構體,可以用層次化的小結構體來(lái)代替;為了提高可移植性,人工對API的結構體增加填充位,這樣,結構體的安排將不會(huì )依賴(lài)與編譯器;在A(yíng)PI的結構體中要謹慎使用枚舉類(lèi)型。一個(gè)枚舉類(lèi)型的大小是編譯器相關(guān)的。 七、位域 注意事項: 1) 應避免使用位域,而使用#define或者enum來(lái)定義屏蔽位; 2) 使用整型邏輯運算AND、OR、“異或”操作和屏蔽對位域進(jìn)行測試、取反和設置操作。這些操作編譯效率高,還可以同時(shí)對多個(gè)位域進(jìn)行測試、取反和設置。 八、邊界不對齊數據和字節排列方式(大/小端) 邊界不對齊數據和字節排列方式這2個(gè)問(wèn)題,可使內存訪(fǎng)問(wèn)和移植問(wèn)題復雜化。須考慮數組指針是否邊界對齊,ARM配置是大端(big-endian),還是小端(little-endian)的存儲器系統。 總結: 1) 盡量避免使用邊界不對齊的數據; 2) 使用類(lèi)型char *可指向任意字節邊界的數據。通過(guò)讀字節來(lái)訪(fǎng)問(wèn)數據,使用邏輯操作來(lái)組合數據,這樣代碼就不會(huì )依賴(lài)于邊界是否對齊或者ARM的字節排列方式的配置; 3) 為了快速訪(fǎng)問(wèn)邊界不對齊的結構體,可以根據指針邊界和處理器的字節排序方式寫(xiě)出不同的程序變體。 九、除法 ARM硬件上不支持除法指令,當代碼中出現除法運算時(shí),ARM編譯器會(huì )調用C庫函數(有符號的除法調用_rt_sdiv,無(wú)符號的調用_rt_udiv),來(lái)實(shí)現除法操作。有許多不同類(lèi)型的除法程序來(lái)適應不同的除數和被除數。 總結: 1) 盡可能避免使用除法。對環(huán)形緩沖區的處理可以不用除法。 2) 如果不能避免除法運算,那么盡可能考慮使用除法程序同時(shí)產(chǎn)生商n/d和余數n%d的好處。 3) 對于重復對同一除數d的除法,預先計算好s=(2k-1)/d?捎贸艘詓的2k位乘法來(lái)代替除以d的k位無(wú)符號整數除法。 4)使用2的整數次冪作除數。當2的整數次冪做除數時(shí),編譯器會(huì )自動(dòng)將除法運算轉換成移位運算。所以在編寫(xiě)程序算法時(shí),盡量使用2的整數次冪做除數。 5)求余運算?梢詫⒁恍┑湫偷那笥噙\算進(jìn)行轉換,以避免在程序中使用除法運算。 如: uint counter1(uint count) { return (++count%60); } 轉換成: uint counter2(uint count) { if (++count >=60) count=0; return (count); } 大多數ARM處理器硬件上并不支持浮點(diǎn)運算。這樣在一個(gè)對價(jià)格敏感的嵌入式應用系統中,可節省空間和降低功耗。除了硬件向量浮點(diǎn)累加器VFP和ARM7500FE上的浮點(diǎn)累加器FPA外,C編譯器必須在軟件上提供浮點(diǎn)支持。 十、內聯(lián)函數和內嵌匯編 高效地調用函數,使用內聯(lián)函數可以完全去除函數調用的開(kāi)銷(xiāo),另外許多編譯器允許在C源程序中使用內嵌匯編。使用包含匯編的內嵌函數,可以使編譯器支持通常不能有效使用的ARM指令和優(yōu)化方法。 內聯(lián)函數和內嵌匯編最大的好處是,可以實(shí)現一些在C語(yǔ)言部分中通常難以完成的操作。使用內聯(lián)函數要比使用#define宏定義更好,因為后者不檢查函數參數和返回值的類(lèi)型。 以下課程可免費試聽(tīng)C語(yǔ)言、電子、PCB、STM32、Linux、FPGA、JAVA、安卓等。 想學(xué)習的你和我聯(lián)系預約就可以免費聽(tīng)課了。 宋工企鵝號:35--24-65--90-88 Tel/WX:173--17--95--19--08 |