众所周知,它与初始化分配的内存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++;
}
}
Run Code Online (Sandbox Code Playgroud)
代码1的输出:
time ./a.out
**real 0m0.287s**
user 0m0.095s
sys 0m0.192s
Run Code Online (Sandbox Code Playgroud)
代码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++;
}
}
Run Code Online (Sandbox Code Playgroud)
代码2的输出:
time ./a.out
**real 0m2.693s**
user 0m0.973s
sys 0m1.721s
Run Code Online (Sandbox Code Playgroud)
更换memset与bzero(buf[i],BLOCK_SIZE)在代码2产生相同的结果.
我的问题是:为什么malloc+ memset比这慢得多calloc?怎么calloc办?
Die*_*Epp 443
简短版本:始终使用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,并通过修改页表将其提供给您的进程.
标准库将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
Run Code Online (Sandbox Code Playgroud)
所以你可以看到,memset()非常快,你不会真正为大块内存获得更好的东西.
将memset()已经归零的内存归零的事实意味着内存被归零两次,但这只能解释2倍的性能差异.这里的性能差异要大得多(我测量幅度超过三个订单我的系统之间malloc()+memset()和calloc()).
编写一个分配内存直到malloc()或calloc()返回NULL 的程序,而不是循环10次.
添加后会发生什么memset()?
Chr*_*utz 12
因为在许多系统上,在备用处理时间内,操作系统自行设置空闲内存为零并标记为安全calloc(),所以当你打电话时calloc(),它可能已经有免费的零内存给你.