ARM下高效C編程
一、 數據類型使用上的優化
1、局部變量
一個char類型的數據比int類型的數據占用更小的寄存器空間或者更小的ARM堆棧空間。這兩種設想對于ARM來說,都是錯誤的。所有的ARM寄存器都是32位的,所有的堆棧入口至少是32位的。當我們執行i++,要利用當i=255后,i++=0這個條件時,可以把它定義為char類型。
二、C循環結構
在ARM上,一個循環其實只要2條指令就足夠了:
一條減法指令,進行循環減法計數,同時設置結果的條件標志;
一條條件分支指令。
這里的關鍵是,循環的終止條件應為減計數到零,而不是計數增加到某個特定的限制值。由于減計數結構已存儲在條件標志里,與零比較的指令就可以省略了。由于不用i作為數組的下標索引,采用減計數就沒有任何問題了。
總而言之,無論對于有符號的循環計數值,都應使用i!=0作為循環的結束條件。對有符號數i,這比使用條件i>0少了一條指令。
總結:
1) 使用減計數到零的循環結構,這樣編譯器就不需要分配一個寄存器來保存循環終止值,而且與0比較的指令也可以省略。
2) 使用無符號的循環計數值,循環繼續的條件為i!=0而不是i>0,這樣可以保證循環開銷只有兩條指令。
3) 如果事先知道循環體至少會執行一次,那么使用do-while循環要比for循環要好,這樣可以使編譯器省去檢查循環計數值是否為零的步驟。
4) 展開重要的循環體可降低循環開銷,但不要過度展開,如果循環的開銷對整個程序來說占的比例很小,那么循環展開反而會增加代碼量并降低cache的性能。
5) 盡量使數組的大小是4或8的倍數,這樣可以容易的以2,4,8次等多種選擇展開循環,而不需要擔心剩余數組元素的問題。
三、 寄存器分配
高效的寄存器分配
應該盡量限制函數內部循環所用局部變量的數目,最多不超過12個,這樣,編譯器就可以把這些變量都分配給ARM寄存器。
四、 函數調用
4寄存器規則
帶有4個或者更少參數的函數,要比多于4個參數的函數執行效率高得多。對帶有少于4個參數的函數來說,編譯器可以用寄存器傳遞所有的參數;而對于多于4個參數的函數,函數調用者和被調用者必須通過訪問堆棧來傳遞一些參數。
如果函數體積很小,只用到很少的寄存器,那么還有一些其他的方法來減少函數調用的開銷。可以把調用函數和被調用函數放在同一個C文件中,這樣編譯器就知道了被調用函數生成的代碼,并以此對調用函數進行一些優化。
總結:
1) 盡量限制函數的參數,不要超過4個,這樣函數調用的效率會更高。也可以將幾個相關的參數組織在一個結構體中,用傳遞結構體指針來代替多個參數。
2) 把比較小的被調用函數和調用函數放在同一個源文件中,并且要先定義,后調用,編譯器就可以優化函數調用或者內聯較小的函數。
3) 對性能影響較大的重要函數可使用關鍵字_inline進行內聯。
五、 指針別名
定義:當2個指針指向同一個地址對象時,這2個指針被稱作該對象的別名(alias)。如果對其中一個指針進行寫入,就會影響從另一個指針的讀出。在一個函數中,編譯器通常不知道哪一個指針是別名,哪一個不是;或哪一個指針有別名,哪一個沒有。
避免指針別名:
1) 不要依賴編譯器來消除包含存儲器訪問的公共子表達式,而應建立一個新的局部變量來保存這個表達式的值,這樣可以保證只對這個表達式求一次值;
2) 避免使用局部變量的地址,否則對這個變量的訪問效率會比較低。
六、 結構體安排
在ARM上使用結構體有2個問題需要考慮:結構體地址邊界對齊和結構體總的大小。
獲得高效結構體的原則:
1) 把所有8位大小的元素安排在結構體的前面;
2) 以此安排16位、32位和64位的元素;
3) 把所有數組和比較大的元素安排在結構體最后。
4) 對于一條指令,如果結構體太大而不能訪問所有的元素,那么把元素組織到一個子結構體中。編譯器可以維持單獨的子結構體的指針。
小結:
結構體元素要按照元素的大小來排列,以最小的元素放在開始,最大的元素安排在最后;避免使用很大的結構體,可以用層次化的小結構體來代替;為了提高可移植性,人工對API的結構體增加填充位,這樣,結構體的安排將不會依賴與編譯器;在API的結構體中要謹慎使用枚舉類型。一個枚舉類型的大小是編譯器相關的。
七、 位域
注意事項:
1) 應避免使用位域,而使用#define或者enum來定義屏蔽位;
2) 使用整型邏輯運算AND、OR、“異或”操作和屏蔽對位域進行測試、取反和設置操作。這些操作編譯效率高,還可以同時對多個位域進行測試、取反和設置。
八、 邊界不對齊數據和字節排列方式(大/小端)
邊界不對齊數據和字節排列方式這2個問題,可使內存訪問和移植問題復雜化。須考慮數組指針是否邊界對齊,ARM配置是大端(big-endian),還是小端(little-endian)的存儲器系統。
總結:
1) 盡量避免使用邊界不對齊的數據;
2) 使用類型char *可指向任意字節邊界的數據。通過讀字節來訪問數據,使用邏輯操作來組合數據,這樣代碼就不會依賴于邊界是否對齊或者ARM的字節排列方式的配置;
3) 為了快速訪問邊界不對齊的結構體,可以根據指針邊界和處理器的字節排序方式寫出不同的程序變體。
九、除法
ARM硬件上不支持除法指令,當代碼中出現除法運算時,ARM編譯器會調用C庫函數(有符號的除法調用_rt_sdiv,無符號的調用_rt_udiv),來實現除法操作。有許多不同類型的除法程序來適應不同的除數和被除數。
總結:
1) 盡可能避免使用除法。對環形緩沖區的處理可以不用除法。
2) 如果不能避免除法運算,那么盡可能考慮使用除法程序同時產生商n/d和余數n%d的好處。
3) 對于重復對同一除數d的除法,預先計算好s=(2k-1)/d。可用乘以s的2k位乘法來代替除以d的k位無符號整數除法。
4)使用2的整數次冪作除數。當2的整數次冪做除數時,編譯器會自動將除法運算轉換成移位運算。所以在編寫程序算法時,盡量使用2的整數次冪做除數。
5)求余運算。可以將一些典型的求余運算進行轉換,以避免在程序中使用除法運算。如:
uint counter1(uint count)
{
}
轉換成:
uint counter2(uint count)
{
}
十、 浮點運算
大多數ARM處理器硬件上并不支持浮點運算。這樣在一個對價格敏感的嵌入式應用系統中,可節省空間和降低功耗。除了硬件向量浮點累加器VFP和ARM7500FE上的浮點累加器FPA外,C編譯器必須在軟件上提供浮點支持。
十一、內聯函數和內嵌匯編
高效地調用函數,使用內聯函數可以完全去除函數調用的開銷,另外許多編譯器允許在C源程序中使用內嵌匯編。使用包含匯編的內嵌函數,可以使編譯器支持通常不能有效使用的ARM指令和優化方法。
評論