簡體   English   中英

為什么malloc + memset比calloc慢?

[英]Why malloc+memset is slower than calloc?

眾所周知, callocmalloc不同之處在於它初始化分配的內存。 使用calloc ,內存設置為零。 使用malloc ,內存不會被清除。

所以在日常工作中,我認為callocmalloc + 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 + memsetcalloc慢得多? 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 工作:

  1. 您的進程調用calloc()並要求256 MiB。

  2. 標准庫調用mmap()並要求256 MiB。

  3. 內核找到256 MiB未使用的RAM,並通過修改頁表將其提供給您的進程。

  4. 標准庫使用memset()將RAM memset()並從calloc()返回。

  5. 您的進程最終退出,內核回收RAM,以便其他進程可以使用它。

它是如何工作的

上面的過程可行,但它不會以這種方式發生。 有三個主要差異。

  • 當您的進程從內核獲取新內存時,該內存可能以前被其他一些進程使用。 這是一種安全風險。 如果該內存有密碼,加密密鑰或秘密莎莎食譜怎么辦? 為了防止敏感數據泄漏,內核總是在將內存提供給進程之前擦除內存。 我們也可以通過歸零來擦除內存,如果新內存歸零,我們也可以將其作為保證,因此mmap()保證它返回的新內存始終為零。

  • 有很多程序可以分配內存,但不會立即使用內存。 有時會分配內存但從未使用過。 內核知道這一點並且很懶惰。 分配新內存時,內核根本不會觸及頁面表,也不會為進程提供任何RAM。 相反,它會在你的進程中找到一些地址空間,記下應該去的地方,並承諾如果你的程序實際使用它,它會把RAM放在那里。 當您的程序嘗試從這些地址讀取或寫入時,處理器會觸發頁面錯誤 ,內核會將RAM分配給這些地址並重新啟動程序。 如果你從不使用內存,頁面錯誤永遠不會發生,你的程序永遠不會真正獲得內存。

  • 某些進程會分配內存,然后從中進行讀取而不進行修改。 這意味着不同進程的內存中的很多頁面可能會填充從mmap()返回的原始零。 由於這些頁面都是相同的,因此內核使所有這些虛擬地址指向一個用零填充的單個共享4 KiB內存頁面。 如果您嘗試寫入該內存,則處理器會觸發另一個頁面錯誤,內核會介入,為您提供一個不與任何其他程序共享的新的零頁面。

最后的過程看起來更像是這樣的:

  1. 您的進程調用calloc()並要求256 MiB。

  2. 標准庫調用mmap()並要求256 MiB。

  3. 內核找到256 MiB的未使用地址空間,記下現在使用的地址空間,然后返回。

  4. 標准庫知道結果mmap()總是充滿着零(或將是 ,一旦它實際上得到一些RAM),所以它不會觸碰內存,所以不存在缺頁,而RAM則沒有給到你的過程。

  5. 您的進程最終會退出,並且內核不需要回收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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM