![edcfb4e9ed514ce65ac8bca808187b15.png](https://i-blog.csdnimg.cn/blog_migrate/894ecdab5c8ba40a50276feaa3d54e00.jpeg)
最近在学习操作系统(OS)的过程中发现一个有趣的现象:参数和变量存放在栈(Stack)中,而动态分配的内存在堆(Heap)中。这样设计的原因呢?我们先来看一看 Stack 和 Heap 的特征比较:
Stack | Heap | |
---|---|---|
Pros | 访问速度快 不需要手动控制内存 | 可以全局访问 没有空间大小限制 |
Cons | 只能存放局部变量 Stack空间有限 | 访问速度较慢 必须手动管理内存 |
所以实际上这两者的分工是比较清楚的。Stack 用来存放一些local variable,随着程序的结束,内存空间也被释放。而在面向对象编程中经过实例化的对象需要进行全局访问,或者是一个申请了动态空间的数组则适合用Heap。
但是注意到作者认为Stack的访问比Heap快,这是令人疑惑的,毕竟两者都存放在内存中。即便因为数据结构不同导致访问速度有差别,这点差别跟 cache miss 来说也不算什么。Stack Overflow上有一个具有实践精神的提问者做了如下实验:
void GetSomeData(char* buffer)
{
// put some data in buffer
}
// char buffer[1024];
int main()
{
char buffer[1024];
while(1)
{
GetSomeData(buffer);
// do something with the data
}
return 0;
}
如果访问Stack和Heap的速度不同,那么 buffer 数组在 main() 函数中生成和作为全局变量生成觉得是不同的访问速度,但是提问者没有观察到加速现象。
答主 Tony Delroy 的回答概括:
总的来说,数据的访问速度取决于数据处于 CPU cache/RAM/swap file 中的那一层,而不是在Stack/Heap中。因为负责fetch data的OS根本不会跟踪数据是来自Stack还是Heap,但是细微的差别确实是有的,从以下三个方面来考虑:
- Allocation:程序花在“分配”和“释放”内存上的时间,包括随着堆使用量增长而偶尔分配的sbrk(或类似的)虚拟地址。
- Access:程序访问全局、堆栈和堆时使用的CPU指令的差异,以及使用基于堆的数据时通过运行时指针的额外间接。
- Layout:某些数据结构(“容器” /“集合”)对缓存更友好(因此速度更快),而某些数据结构的通用实现需要堆分配,并且对缓存的友好程度可能较低。
Allocation and Deallocation
对于全局数据(包括c++名称空间数据成员),虚拟地址通常会在编译时计算和硬编码(可能是绝对值,或者作为段寄存器的偏移量;偶尔它可能需要调整,因为进程是由操作系统加载的)。
对于基于栈的数据,还可以在编译时计算和硬编码堆栈-指针-寄存器-相对地址。当函数被输入和返回时(即在运行时),栈指针寄存器可能会根据函数参数、本地变量、返回地址和保存的CPU寄存器的总大小进行调整。添加更多基于栈的变量只会改变用于调整栈-指针-寄存器的总大小,而不会产生越来越有害的影响。
对于基于堆的数据,运行时堆分配库必须查询并更新其内部数据结构,以跟踪其管理的堆内存块(又名内存池)的哪些部分与该库提供给数据库的特定指针相关联,直到应用程序释放或删除内存。 如果没有足够的虚拟地址空间用于堆内存,则可能需要调用 `sbrk` 这样的OS函数来请求更多的内存(Linux也可以调用mmap为大内存请求创建后备内存,然后在释放/删除时取消映射该内存)。
Access
因为可以在编译时为全局数据和基于堆栈的数据计算绝对虚拟地址或段指针或堆栈指针寄存器的相对地址,所以运行时访问非常快。
对于使用堆托管的数据,程序必须通过运行时确定的指针来访问数据,该指针在堆上保留虚拟内存地址,有时指针与运行时应用的特定数据成员之间会有偏移。 在某些架构上,这可能需要更长的时间。
对于堆访问,指针和堆内存都必须在寄存器中,以便可以访问数据(因此,对CPU高速缓存的需求更多,并且在规模上–更多的高速缓存未命中/故障开销)。
注意:这些成本通常微不足道-甚至不值得一看或三思而后行,除非您要编写对延迟或吞吐量极为重要的内容。
Layout
如果源代码的连续行列出了全局变量,那么它们将被安排在相邻的内存位置中(尽管为了对齐目的可能会进行填充)。 对于同一函数中列出的基于堆栈的变量,也是如此。 这样很好:如果您有X字节的数据,则很可能会发现对于N字节的缓存行,它们很好地打包到了可以使用X / N或X / N + 1缓存行进行访问的内存中。 程序几乎同时需要附近的其他堆栈内容-函数参数,返回地址等,因此缓存非常高效。
当您使用基于堆的内存时,对堆分配库的连续调用可以轻松地将指针返回到不同缓存行中的内存,尤其是在分配大小不同的情况下(例如,三字节分配后跟一个13字节分配),或者 已经进行了大量的分配和释放(导致“碎片化”)。 这意味着当您访问一堆堆分配的小型内存时,最坏的情况是您可能需要在尽可能多的高速缓存行中出错(除了需要加载包含指向堆的指针的内存外)。 堆分配的内存不会与堆栈分配的数据共享缓存行,那里没有协同作用。
此外,C ++标准库不提供更复杂的数据结构(如链表,平衡的二进制树或哈希表),这些数据结构旨在用于基于堆栈的内存中。 因此,在使用堆栈时,程序员倾向于使用内存连续的数组来做他们能做的事情,即使这意味着要进行一些暴力搜索。 高速缓存的效率可能比基于堆的数据容器(将元素分布在更多高速缓存行上)的整体效果更好。 当然,栈的使用不会扩展到大量的元素。并且,如果至少没有使用堆的备份选项,则创建的程序如果要处理的数据量超出预期,则会停止工作。
![56d57786472087b6ea18122aa1f4dd03.png](https://i-blog.csdnimg.cn/blog_migrate/f9f07dbac7641fe925088a0e94602868.png)
参考资料:
7. Memory : Stack vs Heapgribblelab.org Is accessing data in the heap faster than from the stack?stackoverflow.com![3af42f61a18f95a5b51fde7d02b8450e.png](https://i-blog.csdnimg.cn/blog_migrate/b915bd11357b84c0e78cbf8382815e20.png)
.