引言
在 C 語言程序開發(fā)中,提到動態(tài)內(nèi)存分配時(shí)襟雷,基本上每個(gè)程序員都明白 calloc() 和 malloc() 庫函數(shù)的區(qū)別——calloc() 函數(shù)不僅分配內(nèi)存刃滓,還會將分配后的內(nèi)存清零,而 malloc() 函數(shù)則對分配好的內(nèi)存不做任何操作耸弄。
calloc() 函數(shù)的效率比 malloc() + memset() 函數(shù)更高咧虎?
很多 C 語言程序員常把 calloc() 函數(shù)看作是 malloc() + memset() 函數(shù)的組合。不過计呈,今天我在一個(gè)很偶然的測試中發(fā)現(xiàn) calloc() 函數(shù)和 malloc() + memset() 組合函數(shù)的效率差異還是很大的砰诵。請看:
#include<stdio.h>
#include<stdlib.h>
#define BLOCK_SIZE 1024*1024*256
int main()
{
int i=0;
char *buf[10];
while(i<10)
{
buf[i] = (char*)calloc(1,BLOCK_SIZE);
i++;
}
return 0征唬;
}
這段 C 語言代碼調(diào)用了 calloc() 函數(shù)分配了一段內(nèi)存,并且重復(fù) 10 次茁彭,編譯并執(zhí)行之(time 命令可以查看 C 語言程序運(yùn)行消耗的時(shí)間)总寒,得到如下結(jié)果:
# gcc t.c
# time ./a.out
---
real 0m0.287s
user 0m0.095s
sys 0m0.192s
現(xiàn)在將 calloc() 函數(shù)改為 malloc() + memset() 函數(shù),修改后的 C 語言代碼如下理肺,請看:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#define BLOCK_SIZE 1024*1024*256
int main()
{
int i=0;
char *buf[10];
while(i<10)
{
buf[i] = (char*)malloc(BLOCK_SIZE);
memset(buf[i],'\0',BLOCK_SIZE);
i++;
}.
return 0;
}
編譯并執(zhí)行這段 C 語言代碼摄闸,同樣使用 time 命令查看程序運(yùn)行消耗時(shí)間,得到如下結(jié)果妹萨,請看:
# gcc t.c
# time ./a.out
---
real 0m2.693
user 0m0.973s
sys 0m1.721s
應(yīng)該清楚年枕,這兩段 C 語言代碼的工作是一致的,都是分配一段長度為 BLOCK_SIZE 的內(nèi)存并且清零乎完,但是二者消耗的時(shí)間卻相差非常大熏兄,這就有一個(gè)值得深思的問題:calloc() 函數(shù)做了相同的工作,但是效率卻高得多树姨,這是怎么回事呢霍弹?
?? Tips
弄清楚這一點(diǎn),對于我們以后開發(fā)更高效率的 C 語言程序肯定有所幫助娃弓。
解析
在展開討論之前典格,應(yīng)該明白的是以后如果希望申請一段內(nèi)容為 0 的內(nèi)存,則應(yīng)該使用效率更高的 calloc() 函數(shù)台丛,而不是 malloc() + memset() 函數(shù)的組合耍缴。
因?yàn)?calloc() 函數(shù)在內(nèi)部實(shí)現(xiàn)中,會自行判斷分配后的內(nèi)存是否需要清零挽霉,如果某段分配好的內(nèi)存原本就是零防嗡,那么清零動作就免去了。而 malloc() + memset() 函數(shù)的組合則全額做了“分配 + 清零”的動作侠坎,效率自然是有所差異的蚁趁。
一般來說,C 語言程序員應(yīng)該明白四大點(diǎn):程序实胸,標(biāo)準(zhǔn)庫他嫡,內(nèi)核以及頁表
像 malloc() 和 calloc() 這樣的內(nèi)存分配函數(shù)主要用于分配數(shù)百字 KB 以下的內(nèi)存分配,這樣的分配一般是直接從內(nèi)存池(memory pool)中分配的庐完。當(dāng)內(nèi)存池被用完后钢属,或者某段 C 語言代碼一次性請求分配的內(nèi)存超過剩余內(nèi)存池容量時(shí),malloc() 和 calloc() 將直接向內(nèi)核請求內(nèi)存门躯。
內(nèi)核管理每個(gè)進(jìn)程的實(shí)際 RAM淆党,并確保不同進(jìn)程不會干擾彼此的內(nèi)存,這就是所謂的操作系統(tǒng)“內(nèi)存保護(hù)”機(jī)制。有了這樣的機(jī)制染乌,一個(gè)進(jìn)程的崩潰不會導(dǎo)致其他進(jìn)程跟著崩潰山孔,系統(tǒng)的穩(wěn)定性會得到保障。
因此荷憋,在操作系統(tǒng)內(nèi)核的管理下饱须,當(dāng)某段 C 語言代碼需要使用一段內(nèi)存時(shí),它不能直接使用物理內(nèi)存台谊,而只能通過 mmap() 以及 sbrk() 等系統(tǒng)調(diào)用向內(nèi)核申請蓉媳,由內(nèi)核修改頁表為每個(gè)進(jìn)程提供 RAM。
頁表將內(nèi)存地址映射到實(shí)際的物理 RAM锅铅,在 32 位系統(tǒng)上酪呻,進(jìn)程地址(0x00000000 到 0xffffffff)不是實(shí)際的內(nèi)存地址,而是虛擬內(nèi)存地址盐须,處理器將這些地址分為 4KiB 個(gè)頁玩荠,通過頁表,可以將每個(gè)內(nèi)存頁對應(yīng)到不同的物理 RAM 上贼邓。
一些 C 語言程序員認(rèn)為阶冈,calloc() 等內(nèi)存分配函數(shù)是這樣工作的
C 語言程序調(diào)用 calloc() 申請 256KB 內(nèi)存,于是標(biāo)準(zhǔn)庫調(diào)用系統(tǒng)調(diào)用 mmap() 函數(shù)向內(nèi)核申請塑径,內(nèi)核找到 256KB 未被使用的 RAM女坑,并通過修改頁表的方式將其提供給C語言程序,接著標(biāo)準(zhǔn)庫調(diào)用 memset() 函數(shù)將申請到的內(nèi)存清零统舀,然后從 calloc() 函數(shù)將這段內(nèi)存返回匆骗。
之后,當(dāng)這段 C 語言程序退出后誉简,內(nèi)核會回收分配給它的內(nèi)存碉就,以便給其他進(jìn)程使用。
實(shí)際上
上述過程在理論上是可行的闷串,但是實(shí)際上并不會這樣瓮钥。因?yàn)閮?nèi)存總是有限的,內(nèi)核分配給我們的 C 語言程序使用的內(nèi)存可能是之前其他進(jìn)程使用過的烹吵,如果這段內(nèi)存里有密碼碉熄,密鑰,等其他敏感信息呢年叮?
為了避免出現(xiàn)上述安全隱患具被,內(nèi)核總是在將內(nèi)存交給進(jìn)程之前將其清理掉。當(dāng)然了只损,我們也可以自己調(diào)用清零函數(shù)將使用過的內(nèi)存清零,但是不管如何,mmap() 函數(shù)保證其返回的新內(nèi)存是清零后的總是安全的選擇跃惫。
有一些 C 語言程序可能很早就向內(nèi)核申請了一段內(nèi)存叮叹,但是卻不會立刻使用它,甚至可能根本不會使用它爆存。因此在設(shè)計(jì)操作系統(tǒng)內(nèi)核時(shí)蛉顽,為了效率的最大化,可能內(nèi)核在收到內(nèi)存分配請求時(shí)先较,根本不修改頁表携冤,也不向我們的程序提供任何實(shí)際的 RAM。
內(nèi)核可能僅會將一些地址空間標(biāo)記給我們的程序使用闲勺,但是卻不做實(shí)際的分配工作曾棕。這樣就避免了“分配了內(nèi)存,卻沒被使用”帶來的不必要的開銷了菜循。當(dāng)然翘地,一旦 C 語言程序需要讀寫這些地址空間,就會觸發(fā)一個(gè)缺頁異常癌幕,內(nèi)核再將 RAN 真正的分配給這些地址衙耕,并恢復(fù)程序運(yùn)行。
?? Tips
簡而言之勺远,內(nèi)核為了避免不必要的開銷橙喘,實(shí)際的內(nèi)存分配只有在確保真的有 C 語言代碼使用時(shí)(有寫入動作時(shí))才會進(jìn)行。
也有些 C 語言程序分配內(nèi)存后胶逢,可能(不做任何修改)直接就去讀這些內(nèi)存渴杆,這時(shí),內(nèi)核甚至?xí)屵@些 C 語言程序申請的內(nèi)存指向同一個(gè) 4KiB 頁表宪塔,因?yàn)?mmap() 返回的零填充內(nèi)存都一樣磁奖。如果某個(gè)C語言程序嘗試對申請到的內(nèi)存執(zhí)行寫入操作,那么將觸發(fā)另一種缺頁異常某筐,內(nèi)核將為該 C 語言程序分配一個(gè)新的內(nèi)存頁使用比搭,該內(nèi)存頁不與其他任何進(jìn)程共享。
在 C 語言程序開發(fā)中南誊,一次內(nèi)存分配的實(shí)際過程是這樣的
C 語言程序調(diào)用 calloc() 申請 256KB 內(nèi)存身诺,于是標(biāo)準(zhǔn)庫調(diào)用系統(tǒng)調(diào)用 mmap() 函數(shù)向內(nèi)核申請,內(nèi)核找到 256KB 未被使用的地址空間抄囚,記下該地址空閑現(xiàn)在用于什么霉赡,然后返回。
現(xiàn)在標(biāo)準(zhǔn)庫知道 mmap() 返回的結(jié)果總是用零填充幔托,所以它不需要寫入內(nèi)存穴亏,因此不會出現(xiàn)缺頁異常蜂挪,內(nèi)核不必直接實(shí)際分配內(nèi)存。
最后 C 語言程序退出嗓化,內(nèi)核不需要回收內(nèi)存棠涮,因?yàn)閮?nèi)核根本就沒有分配過內(nèi)存。這樣的效率顯然很高刺覆。
如果使用 memset() 將頁面清零严肪,那么 memset() 的寫入動作將觸發(fā)缺頁異常,內(nèi)核將不得不執(zhí)行分配動作谦屑,并執(zhí)行寫入零動作驳糯。這是一項(xiàng)巨大的工作,這也解釋了為什么 calloc() 比 malloc() + memset() 快的原因氢橙。
現(xiàn)在知道原理了酝枢,我們就可以預(yù)言:如果最后使用了庫函數(shù)分配的內(nèi)存,那么 calloc() 函數(shù)可能仍然比 malloc() + memset() 快充蓝,但是二者之前的區(qū)別將不會再那么大隧枫。
應(yīng)該明白
并非所有的操作系統(tǒng)內(nèi)核都具有分頁虛擬內(nèi)存,因此并非在所有平臺上編譯 C 語言代碼都會得到相同的結(jié)果谓苟。calloc() 函數(shù)可能并不從內(nèi)核申請內(nèi)存官脓,而是從共享內(nèi)存池里申請,而共享內(nèi)存池中可能存儲了上一次被使用時(shí)殘留的垃圾數(shù)據(jù)涝焙,calloc() 可以獲取到這些內(nèi)存卑笨,并且調(diào)用 memset() 將其清零。
不同的操作系統(tǒng)管理內(nèi)存很可能是不一樣的仑撞,有些操作系統(tǒng)內(nèi)核會在空閑時(shí)將內(nèi)存歸零赤兴,已備以后需要獲得歸零內(nèi)存時(shí)使用,而有些則不會隧哮,例如 Linux 就不會提前將內(nèi)存清零桶良。