在C語言中函數調用是基於程序堆棧機制,函數調用會進行壓棧操做,堆棧被用來保存函數返回地址、寄存器值、入參、局部變量等信息,在函數調用過程當中須要在棧上分配空間並對棧空間初始化,一個典型的程序堆棧入下圖所示: 函數調用時,首先分配被調用函數的參數空間將其初始化,將其壓入調用者的堆棧中,而後將返回地址壓入調用者的堆棧。被調函數的堆棧開始依次保存了ebp寄存器、其它一些寄存器的值(用於存放上一個函數的部分局部變量)、對於不能存放在寄存器中的局部變量信息將會保存在棧空間中。瞭解了函數棧機制,經過對函數進行適當的調整就能夠顯著的優化性能。html
1:經過修改入參的傳遞方式的優化,一個典型的例子是做爲結構體的入參,以下所示:編程
void Fun1(struct St st);性能優化
void Fun2(struct St *pSt);ide
Fun1須要在被調函數的棧上分配對應大小的空間並將結構體總體拷貝過去。入下圖:函數
Fun2只須要分配一個地址空間而且保存結構體的地址信息,入下圖:性能
2:經過使用非本地跳轉來提高異常返回效率。優化
對於正常的函數調用返回須要一層層的解開函數調用棧,在異常狀況下能夠經過非本地跳轉直接返回到調用點處理,避免解開多層的調用棧。spa
3:經過使用全局變量來替換函數的局部變量能夠避免每次函數調用局部變量空間分配和初始化。設計
4:經過宏來替換函數調用能夠避免棧的開銷。3d
1:推遲計算
在程序中可能存在批量的資源申請,若是批量申請十分耗時而且存在部分資源不會立刻被使用或者可能不會用到,能夠把資源分配的過程推遲到真正須要的時候。一個典型的例子是Linux的寫時拷貝技術,其核心思想就是父子進程共享相同的物理空間,只有進程空間的各段的內容要發生變化時,纔會將父進程的內容複製一份給子進程。下面是一個經過引用記數模擬寫時拷貝的例子:
#include "stdlib.h" typedef struct { int cnt; int data; }DATA; DATA* newData(int data) { DATA* pData = NULL; pData = (DATA*)malloc(sizeof(DATA)); pData->cnt = 1; pData->data = data; return pData; } void unReferData(DATA **ppData) { DATA* pData = NULL; pData = *ppData; if(NULL == pData) { return; } if (1 == pData->cnt) { free(pData); } else { pData->cnt--; } *ppData = NULL; return; } DATA* referData(DATA* pData) { pData->cnt++; return pData; } //copy-on-write void writeValue(DATA **ppData, int value) { DATA* pData = NULL; pData = *ppData; if (1 == pData->cnt) { pData->data = value; } else { pData->cnt--; *ppData = newData(value); } return; } int main() { DATA *pData1 = NULL; DATA *pData2 = NULL; unReferData(&pData1); pData1 = newData(1); unReferData(&pData2); pData2 = referData(pData1); writeValue(&pData2,2); return 0; }
2:預先計算
對於某些要求操做的能夠預測性、低延遲的系統,要提升運行的性能,咱們經過能夠提早獲取資源,在系統啓動過程當中就獲取全部資源而且高效的存放起來,後期直接使用。好比運行過程當中動態內存的申請,其開銷很大而且時間不固定,經過預測系統內存使用量,在啓動期間申請全部動態內存並經過自定義的高效內存池來進行管理。
3:避免計算
一般一些好的編程習慣,可能會致使性能的惡化,好比數據塊的初始化,在代碼中常常能夠看到malloc後立刻memset,而後再對數據塊賦值,若是操做的內存塊很大,對性能影響很明顯。變量初始化是一個好的編程習慣,但若是跟性能衝突儘量避免這樣的操做或者只對關鍵的數據進行初始化,避免大塊數據的操做。
爲了能使 CPU 對變量進行高效快速的訪問,變量的起始地址應該具備某些特性,即所謂的 「 對齊 」 。例如對於 4 字節的 int 類型變量,其起始地址應位於 4 字節邊界上,即起始地址可以被 4 整除。變量的對齊規則以下( 32 位系統):
Type |
Alignment( 默認的天然對齊 ) |
char |
在字節邊界上對齊 |
short(16-bit) |
在雙字節邊界上對齊 |
int long (32-bit) |
在 4 字節邊界上對齊 |
float |
在 4 字節邊界上對齊 |
double |
在 8 字節邊界上對齊 |
structures |
單獨考慮結構體的每一個成員,它們在不一樣的字節邊界上對齊。 其中最大的字節邊界數就是該結構的字節邊界數 |
若是訪問未對齊的地址須要兩個總線週期來訪問兩次內存,而對齊的地址只須要一個總線週期來訪問一次內存。編譯器中提供了#pragma pack(n)來設定變量以n字節對齊方式。n字節對齊就是說變量存放的起始地址的偏移量有兩種狀況:第1、若是n大於等於該變量所佔用的字節數,那麼偏移量必須知足默認的對齊方式,第2、若是n小於該變量的類型所佔用的字節數,那麼偏移量爲n的倍數,不用知足默認的對齊方式。結構的總大小也有個約束條件,分下面兩種狀況:若是n大於全部成員變量類型所佔用的字節數,那麼結構的總大小必須爲佔用空間最大的變量佔用的空間數的倍數;不然必須爲n的倍數。
入下例所示當指定4字節對齊時:
#pragma pack(4)
struct A
{
short a;
int b;
short c;
};
#pragma pack()
假設a的初始化地址爲0,則b的地址爲4,c的地址爲8,總共佔用12個字節空間,爲了對齊浪費了4個字節空間。
入下例所示當指定1字節對齊時:
#pragma pack(1)
struct A
{
short a;
int b;
short c;
};
#pragma pack()
假設a的初始化地址爲0,則b的地址爲2,c的地址爲6,數據緊湊存放節省了存儲空間,但因爲b並無對齊致使對b的操做須要兩次訪問內存。
以上兩種聲明方式分別針對時間和空間性能的優化,但若是咱們合理的設計結構體中的成員位置,則能夠兼顧時間和空間的性能。入下例所示:
#pragma pack(1)
struct A
{
short a;
short c;
int b;
};
#pragma pack()
假設a的初始化地址爲0,則c的地址爲2,b的地址爲4,數據緊湊存放並且各個變量都是對齊的。