編寫高效簡潔的C代碼是軟體工程師的核心追求。程式效率的根本矛盾在於空間(記憶體)與時間(CPU週期)的權衡。本文從實戰經驗總結四大進階技巧,由淺入深,最後附帶必要的警示。
/* 方法A:運行時構建 - 每次調用執行多個函數 */
#define LEN 32
char string1[LEN];
memset(string1, 0, LEN);
strcpy(string1, "This is a example!!");
/* 方法B:編譯期預置 - 直接使用指標,零運行開銷 */
const char string2[LEN] = "This is a example!!";
char *cp = string2; // 直接操作指標
分析:方法B將字串構建移至編譯/載入階段,運行時僅需指標賦值。缺點是靈活性較差(需預知所有字串),優點是極致效率。適用於高即時性、記憶體尚可接受的場合。
/* 方法C:真實函數 - 有呼叫開銷(堆疊保存、現場切換、檢查) */
#define bwMCDR2_ADDRESS 4
#define bsMCDR2_ADDRESS 17
int BIT_MASK(int __bf) {
return ((1U << (bw ## __bf)) - 1) << (bs ## __bf);
}
void SET_BITS(int __dst, int __bf, int __val) {
__dst = ((__dst) & ~(BIT_MASK(__bf))) | (((__val) << (bs ## __bf)) & (BIT_MASK(__bf)));
}
/* 方法D:巨集函數 - 編譯期展開,零呼叫開銷,程式碼膨脹換速度 */
#define BIT_MASK(__bf) (((1U << (bw ## __bf)) - 1) << (bs ## __bf))
#define SET_BITS(__dst, __bf, __val) \
((__dst) = ((__dst) & ~(BIT_MASK(__bf))) | (((__val) << (bs ## __bf)) & (BIT_MASK(__bf))))
核心差異:函數調用需壓棧/彈棧、保存暫存器、檢查堆疊溢出;巨集僅是文字替換,編譯後直接內聯機器碼。高頻調用的小函數最適合巨集化。ARM官方原始碼大量使用此技巧實現高效位操作。
/* 方法E:迴圈累加 - O(n),100次加法+判斷+賦值 */
int i, j = 0;
for (i = 1; i <= 100; i++) {
j += i;
}
/* 方法F:等差數列求和公式 - O(1),1次加法+乘法+除法 */
int i = (100 * (1 + 100)) / 2; // 5050
啟示:演算法複雜度優化遠勝微觀指令優化。開發時應多動腦筋找數學規律,常見技巧:
CPU執行加減位運算通常1週期,乘法3-10週期,除法/取模20-100+週期(視架構)。編譯器對常數除法會優化(乘以倒數近似),但變數除法極慢。
/* 方法G:除法/取模 - 慢 */
int I = 257 / 8; // 32
int J = 456 % 32; // 8
/* 方法H:位運算 - 極快 */
int I = 257 >> 3; // 除以 2^3 = 8
int J = 456 & (32 - 1); // 模 32 = 2^5,等價於 456 & 31
/* 通用公式:x % (2^n) == x & (2^n - 1) */
int K = 456 - ((456 >> 4) << 4); // 另類寫法:減去高位部分
適用條件:除數/模數必須是2的冪次。通用整數除法無位運算等價形式。
| 操作 | 位運算寫法 | 條件 |
|---|---|---|
| 乘以 2n | x << n | 無溢出風險 |
| 除以 2n (無號/正數) | x >> n | 邏輯右移 |
| 除以 2n (有號負數) | (x + (1< | 算術右移向下取整修正 |
| 模 2n | x & ((1< | 無號/正數 |
| 檢查第n位 | (x >> n) & 1 或 x & (1< | |
| 設定第n位 | x |= (1< | |
| 清除第n位 | x &= ~(1< | |
| 翻轉第n位 | x ^= (1< | |
| 取最低設定位 | x & -x | 二補數特性 |
| 清除最低設定位 | x & (x-1) | 經典技巧 |
int 可能16/32/64位,用 uint32_t 等明確型別char src[1024], dst[1024];
/* 方法I:C語言迴圈 - 1024次迴圈,載入/儲存/遞增/判斷 */
for (int i = 0; i < 1024; i++)
dst[i] = src[i];
/* 方法J:ARM內嵌組語 - 128次迴圈,每次載入/儲存8字元 (LDMIA/STMIA) */
#ifdef _ARM_
__asm {
MOV R0, src
MOV R1, dst
MOV R2, #0
loop:
LDMIA R0!, {R3-R10} // 連續載入8個暫存器 (32字節)
STMIA R1!, {R3-R10} // 連續儲存8個暫存器
ADD R2, R2, #32
CMP R2, #1024
BNE loop
}
#endif
效能增益:迴圈次數減少8倍,指令數大幅降低,利用CPU塊傳輸指令與流水線優勢。
memcpy() 不夠用?某些嵌入式場景:資料中可能包含0x00,若誤用字串函數會提前終止;或需特定對齊/總線寬度優化;或裸機環境無標準庫。
-O2/-O3下自動向量化(SSE/AVX/NEON)、循環展開、指令排程,手寫組語極難超越。CLI/STI, MSR/MRS, CPUID)即使如此,也應隔離在平台抽象層(HAL),提供C介面,條件編譯區隔平台。
1. 正確性第一:功能正確、邊界處理、未定義行為消除
2. 演算法優化:O(n²)→O(n log n)→O(n)→O(1) ← 最大收益
3. 資料結構優化:快取友好、記憶體佈局、預取
4. 編譯器優化:-O2/-O3 -march=native -flto + PGO
5. 微觀優化(本文技巧):
- 常數傳播/預計算(空間換時間)
- 數學恒等式替代計算
- 2的冪次除模→位運算
- 熱點函數內聯/巨集化
6. 內嵌組語:最後手段,嚴格隔離,大量註釋,單元測試覆蓋
高效C程式設計不是堆疊技巧,而是在正確層級做正確優化。前三招屬於「軟體工程範疇」,第四招屬於「系統程式設計邊界」。掌握度量工具(profiler, perf, VTune, gprof),用數據說話,避免過早優化——這才是真正的高手之道。