【转】C 语言程序开发中的内存分配究竟是如何进行的?为什么 calloc() 函数的效率比 malloc() + memset() 函数更高?

引言

在 C 语言程序开发中,提到动态内存分配时,基本上每个程序员都明白 calloc() 和 malloc() 库函数的区别——calloc() 函数不仅分配内存,还会将分配后的内存清零,而 malloc() 函数则对分配好的内存不做任何操作。

calloc() 函数的效率比 malloc() + memset() 函数更高?

很多 C 语言程序员常把 calloc() 函数看作是 malloc() + memset() 函数的组合。不过,今天我在一个很偶然的测试中发现 calloc() 函数和 malloc() + memset() 组合函数的效率差异还是很大的。请看:

#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 语言代码调用了 calloc() 函数分配了一段内存,并且重复 10 次,编译并执行之(time 命令可以查看 C 语言程序运行消耗的时间),得到如下结果:

# gcc t.c
# time ./a.out  
---
real 0m0.287s
user 0m0.095s  
sys  0m0.192s 

现在将 calloc() 函数改为 malloc() + memset() 函数,修改后的 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;
}

编译并执行这段 C 语言代码,同样使用 time 命令查看程序运行消耗时间,得到如下结果,请看:

# gcc t.c
# time ./a.out  
---
real 0m2.693   
user 0m0.973s  
sys  0m1.721s 

应该清楚,这两段 C 语言代码的工作是一致的,都是分配一段长度为 BLOCK_SIZE 的内存并且清零,但是二者消耗的时间却相差非常大,这就有一个值得深思的问题:calloc() 函数做了相同的工作,但是效率却高得多,这是怎么回事呢?

📚 Tips

弄清楚这一点,对于我们以后开发更高效率的 C 语言程序肯定有所帮助。

解析

在展开讨论之前,应该明白的是以后如果希望申请一段内容为 0 的内存,则应该使用效率更高的 calloc() 函数,而不是 malloc() + memset() 函数的组合。

因为 calloc() 函数在内部实现中,会自行判断分配后的内存是否需要清零,如果某段分配好的内存原本就是零,那么清零动作就免去了。而 malloc() + memset() 函数的组合则全额做了“分配 + 清零”的动作,效率自然是有所差异的。

一般来说,C 语言程序员应该明白四大点:程序,标准库,内核以及页表

像 malloc() 和 calloc() 这样的内存分配函数主要用于分配数百字 KB 以下的内存分配,这样的分配一般是直接从内存池(memory pool)中分配的。当内存池被用完后,或者某段 C 语言代码一次性请求分配的内存超过剩余内存池容量时,malloc() 和 calloc() 将直接向内核请求内存。

内核管理每个进程的实际 RAM,并确保不同进程不会干扰彼此的内存,这就是所谓的操作系统“内存保护”机制。有了这样的机制,一个进程的崩溃不会导致其他进程跟着崩溃,系统的稳定性会得到保障。

因此,在操作系统内核的管理下,当某段 C 语言代码需要使用一段内存时,它不能直接使用物理内存,而只能通过 mmap() 以及 sbrk() 等系统调用向内核申请,由内核修改页表为每个进程提供 RAM。

页表将内存地址映射到实际的物理 RAM,在 32 位系统上,进程地址(0x00000000 到 0xffffffff)不是实际的内存地址,而是虚拟内存地址,处理器将这些地址分为 4KiB 个页,通过页表,可以将每个内存页对应到不同的物理 RAM 上。

一些 C 语言程序员认为,calloc() 等内存分配函数是这样工作的

C 语言程序调用 calloc() 申请 256KB 内存,于是标准库调用系统调用 mmap() 函数向内核申请,内核找到 256KB 未被使用的 RAM,并通过修改页表的方式将其提供给C语言程序,接着标准库调用 memset() 函数将申请到的内存清零,然后从 calloc() 函数将这段内存返回。

之后,当这段 C 语言程序退出后,内核会回收分配给它的内存,以便给其他进程使用。

实际上

上述过程在理论上是可行的,但是实际上并不会这样。因为内存总是有限的,内核分配给我们的 C 语言程序使用的内存可能是之前其他进程使用过的,如果这段内存里有密码,密钥,等其他敏感信息呢?

为了避免出现上述安全隐患,内核总是在将内存交给进程之前将其清理掉。当然了,我们也可以自己调用清零函数将使用过的内存清零,但是不管如何,mmap() 函数保证其返回的新内存是清零后的总是安全的选择。

有一些 C 语言程序可能很早就向内核申请了一段内存,但是却不会立刻使用它,甚至可能根本不会使用它。因此在设计操作系统内核时,为了效率的最大化,可能内核在收到内存分配请求时,根本不修改页表,也不向我们的程序提供任何实际的 RAM。

内核可能仅会将一些地址空间标记给我们的程序使用,但是却不做实际的分配工作。这样就避免了“分配了内存,却没被使用”带来的不必要的开销了。当然,一旦 C 语言程序需要读写这些地址空间,就会触发一个缺页异常,内核再将 RAN 真正的分配给这些地址,并恢复程序运行。

📚 Tips

简而言之,内核为了避免不必要的开销,实际的内存分配只有在确保真的有 C 语言代码使用时(有写入动作时)才会进行。

也有些 C 语言程序分配内存后,可能(不做任何修改)直接就去读这些内存,这时,内核甚至会让这些 C 语言程序申请的内存指向同一个 4KiB 页表,因为 mmap() 返回的零填充内存都一样。如果某个C语言程序尝试对申请到的内存执行写入操作,那么将触发另一种缺页异常,内核将为该 C 语言程序分配一个新的内存页使用,该内存页不与其他任何进程共享。

在 C 语言程序开发中,一次内存分配的实际过程是这样的

C 语言程序调用 calloc() 申请 256KB 内存,于是标准库调用系统调用 mmap() 函数向内核申请,内核找到 256KB 未被使用的地址空间,记下该地址空闲现在用于什么,然后返回。

现在标准库知道 mmap() 返回的结果总是用零填充,所以它不需要写入内存,因此不会出现缺页异常,内核不必直接实际分配内存。

最后 C 语言程序退出,内核不需要回收内存,因为内核根本就没有分配过内存。这样的效率显然很高。

如果使用 memset() 将页面清零,那么 memset() 的写入动作将触发缺页异常,内核将不得不执行分配动作,并执行写入零动作。这是一项巨大的工作,这也解释了为什么 calloc() 比 malloc() + memset() 快的原因。

现在知道原理了,我们就可以预言:如果最后使用了库函数分配的内存,那么 calloc() 函数可能仍然比 malloc() + memset() 快,但是二者之前的区别将不会再那么大。

应该明白

并非所有的操作系统内核都具有分页虚拟内存,因此并非在所有平台上编译 C 语言代码都会得到相同的结果。calloc() 函数可能并不从内核申请内存,而是从共享内存池里申请,而共享内存池中可能存储了上一次被使用时残留的垃圾数据,calloc() 可以获取到这些内存,并且调用 memset() 将其清零。

不同的操作系统管理内存很可能是不一样的,有些操作系统内核会在空闲时将内存归零,已备以后需要获得归零内存时使用,而有些则不会,例如 Linux 就不会提前将内存清零。


参考

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,063评论 6 510
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,805评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,403评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,110评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,130评论 6 395
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,877评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,533评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,429评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,947评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,078评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,204评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,894评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,546评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,086评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,195评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,519评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,198评论 2 357

推荐阅读更多精彩内容