使用堆内存为什么比栈内存慢很多很多?申请堆内存需要经过操作系统吗?

2021-03-30 14:10:20 +08:00
 LeeReamond

如题,做了一个 C 语言小实验,我分别进行了两个操作,

其一是在堆上申请一个长度为一亿的向量,用来储存 double 类型的数据(包含初始化,全部为 0 )

其二是在一个一万次的循环里重复创建长度为一万的数组,类型同样为 double,同样初始化为 0

理论上两者都涉及到 cpu 在内存上填入一亿个 0,但后者的速度比前者要快 30 多倍。这让我想到一个问题,就是传统都认为堆比较慢,而慢产生的原因是什么,堆和栈都是普通内存,理论上应并无硬件层面的区别。难道程序申请堆内存都必须要经过操作系统才导致速度变慢吗?但是感觉又跟传统经验不符,毕竟经过操作系统是一个非常昂贵的操作,总不可能程序内部所有动态特性都要经过操作系统吧。

不过不经过操作系统的话,又为什么会慢这么多呢?

4769 次点击
所在节点    问与答
43 条回复
BrettD
2021-03-30 14:16:05 +08:00
申请堆内存走 malloc 啊
Glauben
2021-03-30 14:43:31 +08:00
印象中是缓存位置不同,栈是一级缓存,堆是二级缓存,两者速度天差地别
12101111
2021-03-30 14:53:08 +08:00
可以看一下 malloc 都干了什么 https://github.com/microsoft/mimalloc
lsylsy2
2021-03-30 14:55:55 +08:00
其二是在一个一万次的循环里重复创建长度为一万的数组,类型同样为 double,同样初始化为 0

确认是一万个独立的数组吗?听起来有可能循环每次结束,数组都被释放了,所以你实际是反复写了同一个数组一万次。那么缓存等等差距就很显然了。
Justin13
2021-03-30 14:59:29 +08:00
好像栈的开辟是无脑的,不用寻址,地址直接偏移就好,而堆必须扫描找到一个合适的位置。这就费时间了
dalabenba
2021-03-30 15:02:46 +08:00
不是因为堆和栈的问题,他们速度是一样的。是因为 cache,和 pagefault,第一个程序每个都是第一次访问,每个页面都会触发 pagefault,以及 cache miss 会比访问同一段地址高,第二因为在栈上没有 pagefault 的问题,而且放在栈上的话每次循环的地址相同,cache hit 率高
LeeReamond
2021-03-30 15:05:27 +08:00
@lsylsy2 显然是被释放了,栈空间有限怎么可能让你无限写下去。不过这个纯写入操作,缓存影响不大吧
dalabenba
2021-03-30 15:05:50 +08:00
@dalabenba 说叉了,栈上内存不会 pagefault 原因是实际访问的都是同一段,最多只要 pagefault 一次就好
LeeReamond
2021-03-30 15:07:16 +08:00
@Justin13 堆内存逻辑上是连续的,应该只用扫描一次,感觉不是影响的主要原因
LeeReamond
2021-03-30 15:07:56 +08:00
@12101111 看不懂啊大佬
LeeReamond
2021-03-30 15:11:52 +08:00
@dalabenba 大佬,他这个 malloc 需要经过系统吗?学 io 的时候都说最好不要经过系统,开销大。这个 malloc 如果需要经过系统的话,那么系统必然也要经过用户态切换内核态,从 ring3 切换到 ring0 才能操作堆内存吧。如果是这样的话感觉应用程序不应该这么快啊,比如 java 当中也有大量的动态特性,如果全都需要经过操作系统的话开销不是非常大?
ho121
2021-03-30 15:12:37 +08:00
首先需要了解进程的内存结构 https://www.geeksforgeeks.org/memory-layout-of-c-program/

然后了解一下在栈中申请内存的过程 https://stackoverflow.com/a/8629324/1968839

然后了解一下 malloc 是怎么工作的
https://stackoverflow.com/a/3479496/1968839
Whurry
2021-03-30 15:35:19 +08:00
首先是栈空间能这么大吗?栈里应该存不下这些数据。

第二点是进程向操作空间申请内存空间时,一般是先虚拟内存给安排上,访问的时候发现没有对应的物理地址,触发 pagefault,然后才给虚拟地址安排上对应的物理地址。一个页一般是 4KB 。

不确定对不对,仅供参考
LeeReamond
2021-03-30 15:41:34 +08:00
@ho121 感谢,看完了。另外我觉得这些回答写的不对,我又做了个小实验,在一百万次 for 循环里,每次循环新建一块被 malloc 的内存(该内存长度为 2 个 double,所以不会产生过大的问题),统计执行时间一百万次仅为 200ns,如果按照文章中说 malloc 都需要经过系统调用的话,传统一般认为系统调用最短也是百纳秒这个数量级的,怎么可能这么快呢。
misaka19000
2021-03-30 15:48:13 +08:00
在栈上面只需要移动栈指针就可以

在堆上面有一大堆乱七八糟的操作,而且申请堆要走系统调用的
love
2021-03-30 15:54:49 +08:00
代码不放出来看看?

分配机制不一样,栈上肯定不能申请这么大的内存量,栈大小很小的,否则你总听说过 stack overflow 这个词
ho121
2021-03-30 15:55:48 +08:00
@LeeReamond 程序中 malloc 一次一个小的内存,比如 8 字节,实际 malloc 在背后不会老老实实的真的向系统只申请 8 字节,大部分情况下会一次申请较大的内存,以供后面使用。malloc 只有向系统申请内存时,才会用到系统调用
dalabenba
2021-03-30 15:58:59 +08:00
@LeeReamond malloc 是 libc 实现的不是系统调用,底层系统调用的接口是 mmap 或者 brk 。一般 malloc 会预先分配一的内存块,然后把内存块分割,加入到自己的内存池中。当 malloc 自己的内存池中没有足够的内存时候,才用系统调用会从系统中拿。pagefault 会发生在系统调用后拿到内存后,第一次写或者读。所以刚刚说得还不够严谨,要是内存是从内存池中拿,且已经访问过了,而不是系统调用新分配的话,是不会发生 pagefault 的
ho121
2021-03-30 16:00:39 +08:00
@LeeReamond 比如这里有介绍 https://medium.com/@andrestc/implementing-malloc-and-free-ba7e7704a473 sbrk 是 Linux 中的,Windows 系统中应该也有对应的 api
LeeReamond
2021-03-30 16:02:27 +08:00
@love 请参考四楼和七楼,现在是手机操作暂时没法放代码,不过本身逻辑比较简单,我发帖时觉得没有必要放

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/766490

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX