为什么栈内存这么快?

为什么栈内存这么快?

为什么栈内存这么快?

本文基于 YouTube 频道 Core Dumped 视频《WHY IS THE STACK SO FAST?》整理

背景介绍

低级语言(如 C/C++)需要程序员明确指定变量大小。无法定义动态大小的数组,除非给定固定大小。

int arr[10]; // 大小固定,编译期已知

int arr[]; // 编译器报错,不允许

这样限制导致数组大小在程序生命周期内不可变。

栈是什么?

栈是一种 后进先出(LIFO) 数据结构,最后压入的元素最先弹出。

在程序中,栈用来管理函数调用、局部变量。

栈空间大小有限,超出则会产生“栈溢出”。

操作系统与内存管理基础

程序执行时不能随意读写内存,必须由操作系统分配。

操作系统为每个进程分配一块连续的内存空间。

如果访问未分配内存,程序崩溃,报“段错误”(Segmentation Fault)

操作系统分配内存时,并不会精确按照程序请求的字节数分配,而是以较大的连续内存块(chunk)为单位分配。

这块内存块在程序内部可以被进一步管理和细分,但如果程序没有合理组织内存,可能会产生内存碎片。

如果数据没有连续存储,即使总空闲内存足够,也无法分配称为“外部碎片”。

虚拟内存与缓存

操作系统借助磁盘空间(swap)实现虚拟内存,扩展可用内存,但速度远慢于物理内存。

CPU 内置高速缓存(Cache)存储最近使用的数据,提高访问速度。

数据越紧凑,越容易放入缓存,提高缓存命中率,提升程序性能。

栈为何快速?

程序启动时,操作系统为栈预先分配一块固定大小内存区域。

栈由 栈指针(Stack Pointer, SP) 管理,通常存储于 CPU 寄存器。

内存分配只需简单调整指针,无需复杂计算或系统调用。

栈结构示意(内存地址由高到底排列):

+--------------------------+ ← 高地址

| 栈空间 | ← 存储函数局部变量、返回地址等

| |

| ... |

| |

| 栈顶指针 (SP) 指向这里 | ← 当前栈顶位置

+--------------------------+ ← 低地址

入栈时,SP 向低地址方向移动,分配内存。

出栈时,SP 向高地址方向移动,释放内存。

这种顺序保证了高效且无碎片。

函数调用与栈帧

什么是栈帧?

栈帧,也称为活动记录(Activation Record),是函数调用时在栈上为该函数调用分配的一块内存区域。

它存储了函数执行期间所需的所有信息,包括:

函数参数(传入的参数)

返回地址(函数执行完后,程序返回到的指令地址)

具体来说,它存的是一个“内存地址”,即函数调用后,程序执行应继续的指令所在内存的地址(PC,程序计数器)。

这是一个机器码指令的地址,CPU 跳转到这里开始执行后续代码。

局部变量(函数内部定义的变量)

保存的寄存器状态(部分架构下)

调试信息、对齐填充等(具体视编译器和架构而定)

栈帧的结构示意

高地址

+--------------------------+

| 上一个栈帧的内容(调用者) |

+--------------------------+

| 返回地址 | ← 函数执行完跳转回调用点的地址

+--------------------------+

| 函数参数 | ← 传给函数的参数

+--------------------------+

| 保存的寄存器值 | ← 用于恢复调用前CPU状态(部分架构)

+--------------------------+

| 局部变量 | ← 函数内定义的变量

+--------------------------+

| 临时变量 | ← 计算时产生的临时数据

+--------------------------+

| 栈顶指针 (SP) | ← 当前函数栈帧底部(栈顶)

+--------------------------+

低地址

栈帧的生命周期

函数调用时

CPU 将返回地址压入栈中,保存调用位置。

将函数参数按照调用约定压入栈中。

栈指针(SP)移动,给局部变量留出空间。

保存需要保护的寄存器值(调用者保存寄存器)到栈中。

函数执行期间

函数可以访问其局部变量和参数。

使用栈空间存储临时计算结果。

函数返回时

将返回值写入规定的位置(寄存器或栈,小型放寄存器,大型放调用者分配的内存空间)。

恢复寄存器状态。

将栈指针复位到调用前位置,弹出当前栈帧。

跳转到返回地址继续执行调用者代码。

函数调用的示意流程

假设主函数调用 foo(a, b):

调用前栈状态(SP指向栈顶)

+-----------------+

调用 foo:

1. 先把返回地址压入栈(执行完foo要回来的地方)

2. 将参数a, b按约定顺序压入栈

3. 调整栈指针,为foo的局部变量分配空间

4. 进入foo函数体,使用局部变量

执行完foo:

5. foo将返回值放入指定寄存器或栈位置

6. 恢复栈指针,弹出foo的栈帧

7. 跳转到返回地址,继续主函数执行

为什么使用栈帧?

实现函数的调用和返回机制:保存调用现场和返回地址。

管理函数参数和局部变量:让函数内部变量隔离,防止干扰。

支持递归调用:每次递归生成独立栈帧,互不影响。

节省空间和提升性能:通过栈指针移动快速分配和释放内存。

多线程与栈帧

每个线程有自己的独立栈空间和栈帧链。

栈帧结构相同,但栈顶指针独立,互不干扰。

典型调用约定影响

调用约定(Calling Convention)规定参数传递方式(寄存器或栈)、返回值位置、栈清理责任等。

常见约定有 cdecl、stdcall、fastcall 等。

不同约定会影响栈帧布局和大小。

代码示例

int foo(int x, int y) {

int z = x + y; // 局部变量 z

return z * 2;

}

int main() {

int a = 3, b = 4;

int c = foo(a, b);

}

main 调用 foo 时的栈帧布局可能如下:

地址

内容

...

main 的栈帧

返回地址

main 中调用 foo 后继续执行位置

参数 b

4

参数 a

3

foo 的局部变量 z

x + y = 7

...

foo 栈帧底部

栈帧错误示例

栈溢出:递归调用过深,栈帧层层叠加,超出预分配空间。

访问非法栈地址:指针越界或释放后访问导致程序崩溃。

内存分配示例对比

栈内存分配流程

allocate(size):

SP = SP - size

return SP

操作简单,仅移动栈顶指针。

无需搜索空闲块,无碎片问题。

分配速度快。

堆内存分配流程(简化)

allocate(size):

查找合适空闲块

分割空闲块

返回地址

需要复杂数据结构维护空闲块。

可能触发系统调用分配新内存(chunk)。

分配速度慢。

栈的局限性

大小固定,通常几 MB,不能动态增长。

不支持运行时动态大小数组。

深度递归可能导致栈溢出。

多线程程序中,每个线程有独立栈,不能共享。

栈和堆在多线程中的关系与影响

栈(Stack)与多线程

每个线程都有自己的独立栈空间

操作系统为每个新线程分配独立的栈空间,用于存放该线程的函数调用栈帧、局部变量、返回地址等。

→ 线程间栈空间相互隔离,避免竞争。

栈空间大小固定且较小

线程栈通常是有限的(比如几MB),过多线程或深度递归会导致栈溢出(Stack Overflow)。

→ 需要合理控制线程数量和递归深度。

栈上的数据只在线程内部有效

栈是线程私有的,局部变量在函数结束后释放,不能跨线程访问。

栈空间增长/缩小通常受限

栈大小在创建线程时确定,不同系统/环境有不同默认大小,可设置。

堆(Heap)与多线程

所有线程共享同一个堆

堆是进程级资源,所有线程共同使用,用于动态分配内存(new/malloc)。

堆分配是线程安全的(但开销较大)

现代内存分配器(如glibc malloc、jemalloc、tcmalloc)通常实现了多线程安全机制,避免多线程同时操作堆时出现数据竞争。

→ 这可能导致锁竞争或性能下降。

并发分配/释放带来的碎片和锁竞争问题

多线程频繁分配释放堆内存可能导致碎片化加剧,内存分配器需设计高效算法减少锁粒度和竞争。

线程局部存储(Thread Local Storage, TLS)

为减少堆上的锁竞争,有些程序会采用 TLS,给每个线程维护独立的内存池或缓存。

多线程下栈和堆的协作示意

线程1: 线程2: 线程3:

+---------+ +---------+ +---------+

| 栈空间1 | | 栈空间2 | | 栈空间3 | <-- 独立

+---------+ +---------+ +---------+

\ | /

\ | /

\ | /

+----------------+

| 堆(Heap) | <-- 共享

+----------------+

主要影响与建议

方面

说明

建议

栈大小

栈空间小,线程多时容易溢出

适当调整线程栈大小,避免深度递归

堆内存分配

多线程访问堆存在锁竞争,可能影响性能

使用高效的多线程内存分配器,或 TLS

数据安全

栈数据线程私有,堆数据共享需同步

跨线程访问堆数据需做好同步和线程安全

内存碎片

多线程频繁堆分配/释放可能导致内存碎片

减少频繁小内存分配,使用对象池等技术

线程局部缓存

为减少堆锁竞争,分配器常用线程局部缓存提高性能

利用 TLS 或专用内存池

总结

栈因预分配固定内存和简单指针操作而非常快速。

紧凑内存布局提高 CPU 缓存命中率,进一步提升性能。

适合存储局部变量和短生命周期数据。

堆适合动态、生命周期复杂的数据,分配更慢,且涉及系统调用分配大块chunk。

相关推荐

十大高质量自学网站 学习网站哪个好 网上学技能的网站→MAIGOO生活榜
成交转化率多少才算合格?100个访客成交几个算正常?
兴盛帝国 (Flourishing Empires)中文版v2.1
365彩票app下载2020

兴盛帝国 (Flourishing Empires)中文版v2.1

08-24 🌱 8723