看到一篇讲到关于 C/C++ calloc() 一些内幕的好文,本文简单记录分享一下。

1、calloc()

比较广为人知的部分,malloc()calloc() 都是分配内存的函数,malloc() 返回未初始化的内存;而 calloc() 相当于调用了 malloc 然后调用 memset 来给分配的内存填充 0。

1
2
void* buffer1 = malloc(size);
void* buffer2 = calloc(count, size);

The calloc() function contiguously allocates enough space for count objects that are size bytes of memory each and returns a pointer to the allocated memory. The allocated memory is filled with bytes of value zero.

既然这样,为什么不直接用 malloc + memset 来代替 calloc 呢?

1.1、差异

calloc(count, size) 是通过 count * size 相乘结果来决定分配多少内存的,但它同时还会检查结果是否溢出,溢出则会返回 NULL,整数溢出可能会导致实际上分配的内存是很小的而没发现到;而 malloc 不会这样检查,因为它接收的是单个参数,一个用户认为设置没问题的参数。

上面只是一个安全性相关的差异,而两者还有性能上的差异。在文中测量 1GB 分配/释放的基准测试中,calloc + freemalloc + memset + free 快了 100 倍以上。(注:只是因为需要用到 memset,不然只使用 malloc 会更快,并非所有场景都需要填充 0 初始化内存,比如读文件到内存中,可以直接覆盖到。)

这是因为 calloc 作弊了,对于小内存分配(glibc 的区分是 128kB),calloc 的调用与使用 malloc + memset 一样,速度基本一致;但对于大内存分配时,差异出现了。

大内存的分配,一般需要内存分配器去向操作系统去申请分配,当操作系统将内存分配给进程时,它总是先将其清零,因为否则的话我们就可以看到其他进程使用到的内存遗留痕迹。所以对于这种已经清零的内存,没有必要再调用 memset。这个作为开发者无法直接判断,但 calloc 实现在标准库内存分配器当中,因此它可以判断返回的内存是否是来自操作系统的新内存。

除了新内存不做重复的初始化内存操作,calloc 分配的内存是写时复制的,当第一次写入时才会对应到 RAM 真实内存页。而 malloc 由于使用了 memset,就需要立刻付出内存分配映射的代价。

注:由于这个优化并未在官方函数标准文档里说明,所以还需要区分,是依平台而定的。

参考