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

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

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

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

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

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

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

5148 次点击
所在节点    问与答
43 条回复
godblessumilk
2021-03-30 16:17:41 +08:00
土字旁的 [堆] 是一盘散沙。木字旁的 [栈] 是码放得整整齐齐的木板,一捧沙里找一颗沙子快,还是一摞木板里找一块木板快?
godblessumilk
2021-03-30 16:21:45 +08:00
可以查查英文单词 stack 和 heap 的区别,就明白了。话说中文里 stack 翻译成堆,heap 翻译成栈,还挺贴切的
LeeReamond
2021-03-30 16:27:08 +08:00
@godblessumilk 兄弟你这个堆栈自己都能写错就别来强答了吧。。
3dwelcome
2021-03-30 16:29:31 +08:00
@LeeReamond "不过本身逻辑比较简单,我发帖时觉得没有必要放"
编译器会优化掉很多,你写出来的代码,不一定是最后运行的代码。比如只 new 不填充,系统是不会给你真正分配内存的。

个人经验,影响内存速度有三点。
1 。 寻址空间大小,我有一个程序,相同的代码,访问 256M 之内时超快,访问 4G 空间时,速度下降一半。
2 。内存对齐,x86 访问非对齐内存,会有额外多余开销。
3 。就是 malloc(堆)和 alloca(栈)区别了,后者是真的快,用飞速也不为过。
aeon113
2021-03-30 16:42:49 +08:00
贴一下你的代码和编译选项吧

malloc 执行过程中是有可能会进入到内核态的,并且,我记得在 Linux 中,给用户进程分配出的虚拟地址事实上并没有对应物理内存,物理地址会在目标 page 第一次被访问时分配。这个可能会造成进程在写入大数组时又多次陷入内核态。
可以尝试 malloc 一次,然后多写几次,丢掉第一次写入的测试数据,用剩下的写入延迟算出一个平均值做结果。

另外,这里栈上的写入过程相当于对同一段栈内存写入了 10000 次。如果不是用 memset 来写的话,那有可能前 9999 次全部被优化掉了只剩下了最后一次。这个得看编译器的生成结果才能确定。

数组大小不同,占用的 CPU cache line 数量也不同。一块 CPU 不是只有一个进程在使用,每个进程对内存的每次读写都可能造成某个 cache line 内的原数据被刷出,新数据被读入。那么数据 size 越大,占用的 cache line 越多,其内部分数据被刷出的概率也就越高,相对性能也就会更差一些。

最后,如果机器内存不大的话,访问堆内存时也可能会因为 swap 和刷 dirty page 损失不少性能。
hyyou2010
2021-03-30 20:12:32 +08:00
1,栈增加是索引地址加加,栈归还就是索引地址减减,几乎没有更快的空间分配方式
2,堆分配空间则是从整个堆空间寻找一片合适的,这个并不简单,要防止空闲空间成碎片以至于后续无法使用。归还时还得合并空闲空间,也是一笔很大的开支
IsaacYoung
2021-03-30 20:27:46 +08:00
sbrk 更新页表
kejialiu
2021-03-30 23:17:14 +08:00
还有寄存器优化。C 语言很多局部变量原则上是分配在栈上,但编译器优化后直接放通用寄存器就好了,都不需要进内存
iceheart
2021-03-30 23:54:37 +08:00
以上一个答对的都没有。
栈比堆快只有一个原因: 栈一直在访问,没有机会被换出 cpu 缓存,所以栈内存访问总是高速缓存命中,快 30 倍是 cpu 高速缓存与内存实际访问速度的差距
irytu
2021-03-31 01:19:31 +08:00
堆内存分配挺复杂的 有可能会 trap 到内核里 在地址空间不够的情况下 需要 sbrk syscall 来增长 然后第一次访问的时候 page fault,其实栈也有类似的情况如果没理解错 栈的不够用了 也会 trigger 一次 page fault 更新 page table 直到 stack overflow ?😂

但是差距就在栈的分配比堆简单很多,堆分配我记得挺复杂的,csapp 上有讲过类似的例子,实际还要考虑楼上提到的地址空间碎片化的问题,分配的效率等,一般根据实现不同可能会有 O(n), O(logN) 这种时间开销

https://stackoverflow.com/a/283164
irytu
2021-03-31 01:25:32 +08:00
刚刚查了一下 似乎 sbrk 这个 syscall 渐渐被废弃了 用 mmap(2) 取而代之
gyf304
2021-03-31 01:36:00 +08:00
你这个控制变量都没有,heap 比 stack 大那么多,cache miss 肯定很严重啊。
还有就是如上面说的,mmap 要经过内核,初次写入被分配的页的时候也是是要 page fault 的,也是要过内核的。
建议读一下《 CS:APP 》关于内存的内容
gyf304
2021-03-31 01:40:11 +08:00
@2435043xia 栈和堆内存物理上是没有区别的
gyf304
2021-03-31 01:47:10 +08:00
@godblessumilk
我个人认为用比喻的方式学习是比较糟糕的方式。入门可以,拿来解释东西不太妥当。“因为 A 像 B,所以我们用 B 来解释 A”是不合逻辑的。
堆和栈内存在物理上是没有区别的。
geelaw
2021-03-31 03:06:17 +08:00
为什么楼上都有这么多答案…

这个问题会因为楼主使用了何种 C 语言编译器、如何申请的堆内存有千奇百怪的答案。如果楼主用的 malloc 内部维护一个结构,且优先考虑直接分配刚刚释放的内存,且没有发生优化,是不可能会有这么大的差别的。
xiadong1994
2021-03-31 05:43:18 +08:00
可能的影响太多了,编译器优化,malloc 实现都没考虑。先看看优化后的汇编是不是你想的那样。
myy1966
2021-03-31 09:39:21 +08:00
https://github.com/spuhpointer/stack-vs-heap-benchmark
都带数据初始化的话,堆和栈的速度几乎一样。你对栈的程序跟堆不同,栈上在循环里创建数组的话每次到下一个循环会把上一次创建的数据销毁,然后重新创建数组,这样你的数组实际上一直在使用一块相同的内存空间,这增大了缓存命中的概率。而你的堆的程序直接创建了长度为 1 亿的数据,这样连续的访存会产生很多缓存不命中,降低了性能。公平的对比办法是手动把操作系统预设的栈大小改大到可以容下 1 亿长度的数组,然后在栈的测试中直接创建长度为 1 亿的数组,这样测试出来的性能堆和栈应该是差不多的。
3dwelcome
2021-03-31 09:59:56 +08:00
```
double* test1 = new double[10000*10000];
for (int i=0;i<10000*10000;i++)
test1[i] = 0;
```

```
double* test2 = (double*)alloca(10000);
for (int y=0;y<10000;y++)
for (int x=0;x<10000;x++)
{
test2[i] = 0;
}
```

我的堆 /栈测试代码,test1 堆运行完是 0.4s, test2 栈是 0.2s ,很符合自己盲猜结果。哪有楼主说的 30 倍差距,肯定是有什么 bug 。
3dwelcome
2021-03-31 10:05:23 +08:00
@gyf304 "栈和堆内存物理上是没有区别的", CPU L1/L2 高速缓存和普通内存地址访问,速度区别巨大。
dalabenba
2021-03-31 10:22:27 +08:00
@3dwelcome cache 对于 cpu 访问内存是透明的,栈和堆只不过是进程地址空间划分的两个部分,下面映射的物理内存都是一样的

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

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

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

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

© 2021 V2EX