[英]Why malloc+memset is slower than calloc?
眾所周知, calloc
與malloc
不同之處在於它初始化分配的內存。 使用calloc
,內存設置為零。 使用malloc
,內存不會被清除。
所以在日常工作中,我認為calloc
是malloc
+ memset
。 順便說一下,為了好玩,我為基准編寫了以下代碼。
結果令人困惑。
代碼1:
#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++;
}
}
代碼1的輸出:
time ./a.out
**real 0m0.287s**
user 0m0.095s
sys 0m0.192s
代碼2:
#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++;
}
}
代碼2的輸出:
time ./a.out
**real 0m2.693s**
user 0m0.973s
sys 0m1.721s
在代碼2中用bzero(buf[i],BLOCK_SIZE)
替換memset
會產生相同的結果。
我的問題是:為什么malloc
+ memset
比calloc
慢得多? calloc
怎么做呢?
簡短版本:始終使用calloc()
而不是malloc()+memset()
。 在大多數情況下,它們將是相同的。 在某些情況下, calloc()
會做更少的工作,因為它可以完全跳過memset()
。 在其他情況下, calloc()
甚至可以作弊而不分配任何內存! 但是, malloc()+memset()
將始終完成全部工作。
理解這一點需要對內存系統進行簡短的瀏覽。
這里有四個主要部分:程序,標准庫,內核和頁表。 你已經了解你的程序,所以......
像malloc()
和calloc()
這樣的內存分配器主要用於獲取小的分配(從1字節到100的KB),並將它們分組到更大的內存池中。 例如,如果分配16個字節, malloc()
將首先嘗試從其中一個池中獲取16個字節,然后在池運行時從內核請求更多內存。 但是,由於你要問的程序是同時分配大量內存, malloc()
和calloc()
只是直接從內核請求內存。 此行為的閾值取決於您的系統,但我已經看到1 MiB用作閾值。
內核負責為每個進程分配實際的RAM,並確保進程不會干擾其他進程的內存。 這被稱為內存保護,自20世紀90年代以來一直很常見,這就是為什么一個程序在不關閉整個系統的情況下崩潰的原因。 因此,當程序需要更多內存時,它不僅可以占用內存,而是使用系統調用(如mmap()
或sbrk()
從內核請求內存。 內核將通過修改頁表為每個進程提供RAM。
頁表將內存地址映射到實際物理RAM。 在32位系統上,進程的地址0x00000000到0xFFFFFFFF不是實際內存,而是虛擬內存中的地址。 處理器將這些地址划分為4個KiB頁面,並且可以通過修改頁面表將每個頁面分配給不同的物理RAM。 只允許內核修改頁表。
以下是如何分配256 MIB 不工作:
您的進程調用calloc()
並要求256 MiB。
標准庫調用mmap()
並要求256 MiB。
內核找到256 MiB未使用的RAM,並通過修改頁表將其提供給您的進程。
標准庫使用memset()
將RAM memset()
並從calloc()
返回。
您的進程最終退出,內核回收RAM,以便其他進程可以使用它。
上面的過程可行,但它不會以這種方式發生。 有三個主要差異。
當您的進程從內核獲取新內存時,該內存可能以前被其他一些進程使用。 這是一種安全風險。 如果該內存有密碼,加密密鑰或秘密莎莎食譜怎么辦? 為了防止敏感數據泄漏,內核總是在將內存提供給進程之前擦除內存。 我們也可以通過歸零來擦除內存,如果新內存歸零,我們也可以將其作為保證,因此mmap()
保證它返回的新內存始終為零。
有很多程序可以分配內存,但不會立即使用內存。 有時會分配內存但從未使用過。 內核知道這一點並且很懶惰。 分配新內存時,內核根本不會觸及頁面表,也不會為進程提供任何RAM。 相反,它會在你的進程中找到一些地址空間,記下應該去的地方,並承諾如果你的程序實際使用它,它會把RAM放在那里。 當您的程序嘗試從這些地址讀取或寫入時,處理器會觸發頁面錯誤 ,內核會將RAM分配給這些地址並重新啟動程序。 如果你從不使用內存,頁面錯誤永遠不會發生,你的程序永遠不會真正獲得內存。
某些進程會分配內存,然后從中進行讀取而不進行修改。 這意味着不同進程的內存中的很多頁面可能會填充從mmap()
返回的原始零。 由於這些頁面都是相同的,因此內核使所有這些虛擬地址指向一個用零填充的單個共享4 KiB內存頁面。 如果您嘗試寫入該內存,則處理器會觸發另一個頁面錯誤,內核會介入,為您提供一個不與任何其他程序共享的新的零頁面。
最后的過程看起來更像是這樣的:
您的進程調用calloc()
並要求256 MiB。
標准庫調用mmap()
並要求256 MiB。
內核找到256 MiB的未使用地址空間,記下現在使用的地址空間,然后返回。
標准庫知道結果mmap()
總是充滿着零(或將是 ,一旦它實際上得到一些RAM),所以它不會觸碰內存,所以不存在缺頁,而RAM則沒有給到你的過程。
您的進程最終會退出,並且內核不需要回收RAM,因為它從未首先分配過。
如果使用memset()
將頁面歸零, memset()
將觸發頁面錯誤,導致RAM被分配,然后將其歸零,即使它已經填充了零。 這是一項巨大的額外工作,並解釋了為什么calloc()
比malloc()
和memset()
更快。 如果最終還是使用內存, calloc()
仍然比malloc()
和memset()
更快,但差別並不是那么荒謬。
並非所有系統都有分頁虛擬內存,因此並非所有系統都可以使用這些優化。 這適用於非常老的處理器,如80286以及嵌入式處理器,這些處理器對於復雜的內存管理單元來說太小了。
這也不總是適用於較小的分配。 使用較小的分配, calloc()
從共享池獲取內存,而不是直接進入內核。 通常,共享池可能有舊存儲器中存儲的垃圾數據,該存儲器使用並通過free()
釋放,因此calloc()
可以獲取該存儲器並調用memset()
來清除它。 常見的實現將跟蹤共享池的哪些部分是原始的並且仍然用零填充,但並非所有實現都這樣做。
根據操作系統的不同,內核在空閑時間內可能會或可能不會將內存歸零,以防您以后需要獲取一些歸零內存。 Linux並沒有提前將內存歸零,而Dragonfly BSD最近也從內核中刪除了這個功能 。 但是,其他一些內核會提前做零內存。 無論如何,空閑的歸零頁面還不足以解釋大的性能差異。
calloc()
函數沒有使用memset()
一些特殊的內存對齊版本,並且無論如何都不會使它更快。 現代處理器的大多數memset()
實現看起來像這樣:
function memset(dest, c, len)
// one byte at a time, until the dest is aligned...
while (len > 0 && ((unsigned int)dest & 15))
*dest++ = c
len -= 1
// now write big chunks at a time (processor-specific)...
// block size might not be 16, it's just pseudocode
while (len >= 16)
// some optimized vector code goes here
// glibc uses SSE2 when available
dest += 16
len -= 16
// the end is not aligned, so one byte at a time
while (len > 0)
*dest++ = c
len -= 1
所以你可以看到, memset()
速度非常快,你並沒有真正為大塊內存獲得更好的東西。
memset()
將已經歸零的內存歸零的事實意味着內存被歸零兩次,但這只能解釋2倍的性能差異。 這里的性能差異要大得多(我在malloc()+memset()
和calloc()
之間的系統上測量了三個數量級以上)。
編寫一個分配內存的程序,直到malloc()
或calloc()
返回NULL,而不是循環10次。
如果添加memset()
會發生什么?
因為在許多系統上,在備用處理時間內,操作系統自行設置空閑內存為零並標記為calloc()
安全,所以當你調用calloc()
,它可能已經有免費的,零內存給你。
在某些模式的某些平台上,malloc在返回之前將內存初始化為某些通常為非零的值,因此第二個版本可以很好地初始化內存兩次
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.