操作系统知识体系
一. 内存管理
1. 堆 (Heap) 与 栈 (Stack) 的本质区别
-
核心逻辑: 栈是操作系统自动分配释放的,连续且速度极快;堆是手动申请的(如
new/malloc),不仅慢,还容易产生碎片。 -
游戏场景: 为什么我们要极力避免在
Tick(每帧更新)里频繁new对象?因为堆分配会陷入内核态,且可能引发内存碎片化。像子弹对象池这种技术,本质上就是把“运行时的堆分配”转化为了“初始化时的集中分配”,后续复用内存,从而避开堆操作和 GC(垃圾回收)开销。 -
面试高频: 栈溢出(Stack Overflow)一般是怎么导致的?(答:无限递归调用或在栈上分配了过大的局部数组/结构体)。
2. 内存对齐 (Memory Alignment)
-
核心逻辑: CPU 读取内存不是一个字节一个字节读的,而是按块(Cache Line,通常是 64 字节)读取。如果一个数据结构跨越了两个 Cache Line,CPU 就需要读两次。
-
游戏场景: 在 C++ 定义底层通信结构体(如网络同步的历史帧状态)或高频计算的数学向量时,合理排列成员变量的顺序(按占用空间从大到小,或利用
#pragma pack/alignas),可以大幅缩减结构体体积,提升 CPU Cache 缓存命中率。 -
面试高频: 给定一个 struct 结构体(包含
char,int,double),问它在 64 位系统下占用多少字节?(注意对齐规则和填充字节 padding)。
3. 虚拟内存与缺页中断 (Page Fault)
-
核心逻辑: 进程以为自己拥有整块连续内存,其实是系统通过页表 (Page Table) 映射到物理内存上的。
-
游戏场景: 游戏在无缝大地图中奔跑时,突然发生严重的卡顿(掉帧)。底层原因之一可能是硬缺页中断(Major Page Fault)——CPU 访问的虚拟内存页当前不在物理内存中,操作系统必须阻塞当前线程,去磁盘(硬盘)里把资源(比如远处的贴图或模型)加载到物理内存里。
二、CPU底层原理
2.1 Cache Line(缓存行)
Cache Line(缓存行) 是 CPU 高速缓存(Cache)中数据传输的最小单位。
它是连接“极快的CPU”和“较慢的内存”之间的桥梁。理解它,是理解你简历中 alignas(64) 优化原理的关键。
🏗️ 核心定义
- 最小传输块: CPU 不能只从内存里取 1 个字节或 1 个整数。当 CPU 发现 L1 Cache 里没有数据时,它会向内存发起请求,一次性拉取一整块数据到 Cache 中。这块数据就是 Cache Line。
- 标准大小:
- 在 x86-64 架构(绝大多数现代 PC、服务器、游戏主机)中,Cache Line 的大小固定为 64 字节 (Bytes)。
- 这就是为什么你的代码中使用了
alignas(64)—— 为了让每个对象独占一条 Cache Line。
💡 通俗比喻:超市购物
想象 CPU 是顾客,内存是仓库,Cache 是购物车。
-
传统做法(指针乱序): 你想买一个苹果(访问一个对象)。 你去仓库,仓库管理员说:“我们这里一次只能按整箱发货”。一箱有 64 个苹果(Cache Line)。 结果:你为了拿 1 个苹果,不得不把整整一箱(64 个苹果,其中可能只有 1 个是你需要的,其他都是垃圾数据)都搬回购物车。
- 浪费:你只用了 1/64 的数据,但付出了搬运整箱的时间和精力(带宽浪费)。
- 后果:如果下一个你要买的苹果在仓库的另一端,你得再跑一趟,再搬一箱。
-
你的优化(Cache Line 对齐): 你把所有需要的苹果(
TSlot结构体)整齐地码放在仓库里,确保每个苹果都独占一个箱子,且箱子之间紧挨着。- 效果:当你去拿第 1 个苹果时,仓库不仅给了你第 1 个苹果的箱子,还因为预取机制,顺手把第 2、3、4 个苹果的箱子也搬过来了(因为它们在物理上紧挨着)。
- 命中率飙升:当你需要第 2 个苹果时,它已经在购物车(L1 Cache)里了,不需要再去仓库跑一趟。
🔍 为什么必须是 64 字节?
这是硬件设计的妥协与平衡:
- 太小(如 8 字节):每次访问都要频繁往返内存,总线拥堵,延迟高。
- 太大(如 256 字节):虽然减少了一次往返,但如果你只需要其中 1 个字节,剩下的 255 字节就浪费了宝贵的 Cache 空间(Cache Capacity),导致其他重要数据被挤出 Cache。
- 64 字节:经过几十年验证,这是空间局部性(Spatial Locality)的最佳平衡点。大多数程序访问的数据往往聚集在一起,64 字节足够覆盖一个小数组或几个相关变量。
⚠️ 致命陷阱:伪共享 (False Sharing)
这是你简历中提到的关键点!
场景: 假设你有两个线程,分别修改 A 和 B 两个变量。
A和B在内存中紧挨着,都在同一个 64 字节 Cache Line 内。- 线程 1 修改
A-> CPU 标记这条 Cache Line 为“脏数据”,并通知其他核心:“我要改这个线了,你们别动!” - 线程 2 修改
B-> CPU 发现这条线已经被线程 1 锁定了,必须等线程 1 释放,或者强制刷新缓存。 - 结果:尽管 A 和 B 是完全独立的变量,但因为它们在同一条 Cache Line 上,CPU 会认为它们互相干扰。两个线程会疯狂地在彼此的核心间同步状态(MESI协议),导致性能暴跌,甚至比单线程还慢!
你的解决方案:
alignas(64) TSlot通过强制对齐,你确保每个
TSlot都独占一条 64 字节的 Cache Line。
- Slot 0 占 Line 0。
- Slot 1 占 Line 1。
- ...
这样,即使两个线程同时修改不同的 Slot,它们操作的是不同的 Cache Line,完全互不干扰,彻底消除了伪共享。
📊 总结对比
| 特性 | 没有对齐 (传统方案) | 对齐后 (你的方案) |
|---|---|---|
| 内存布局 | 对象分散,大小不一 | 连续排列,每个对象占满 64 字节 |
| Cache Line 利用率 | 低(一个 Line 里塞进多个小对象,或跨行) | 高(一个对象独占一个 Line,无浪费) |
| 伪共享风险 | 极高(多线程下性能灾难) | 零(每个线程操作独立 Line) |
| 预取效率 | 差(随机跳转,无法预测) | 极好(顺序访问,预取器完美工作) |
| 性能表现 | 慢(大量等待内存) | 快(全速运行) |
最大化空间布局
这个概念在初学底层优化时确实有些反直觉,但它是现代 CPU 性能调优(尤其是游戏引擎开发)的核心密码。
要理解“最大化空间局部性”,我们只需要弄懂一个核心事实:CPU 计算的速度,远远大于它从内存(RAM)拿数据的速度。
为了不让 CPU 干等,硬件工程师设计了高速缓存(Cache)。接下来,我们用一个“厨房做菜”的通俗比喻,结合你项目的“子弹”场景,把这个概念彻底拆解。
1. 核心前置知识:CPU 怎么拿数据?(Cache Line)
CPU 从内存读取数据时,绝对不会“要一个字节,拿一个字节”。
这就好比厨师(CPU)需要一个土豆,他不会让搬运工去仓库(RAM)只拿一个土豆。因为跑一趟太费时间了。
搬运工每次去仓库,都会装满一个固定大小的箱子带回来。这个箱子在计算机里叫 Cache Line(缓存行),通常大小是 64 字节。
不管你只要 1 个字节还是 4 个字节,CPU 都会一口气把包含目标数据的连续 64 个字节全部搬到高速缓存(L1 Cache)里。
2. 通俗比喻:什么是“空间局部性”?
空间局部性(Spatial Locality)指的是一种规律:如果你访问了内存中的某个位置,那么它相邻的位置很快也会被访问。
-
没有空间局部性(原生的 AActor):
假设我们要更新 1000 颗子弹的位置。传统的面向对象写法(AoS - 数组结构体)是把子弹的所有属性绑在一起。
内存排布就像:
[位置, 速度, 贴图指针, 音频组件, 碰撞体] | [位置, 速度, 贴图指针, 音频组件, 碰撞体]...当 CPU 要算第一颗子弹的“位置”时,搬运工搬回来一个 64 字节的箱子。结果箱子里只有 1 个“位置”,剩下的空间装的是“贴图、音频”这些当前计算根本不需要的垃圾数据。
算第二颗子弹时,又要重新去仓库搬箱子(这就是 Cache Miss,缓存未命中,导致 200+ CPU 周期被浪费在等待上)。
-
最大化空间局部性(你的 SoA 优化方案):
你把所有子弹的同类数据剥离出来,存放在连续的数组里(SoA - 结构体数组)。
内存排布变成了:
[位置1, 位置2, 位置3, 位置4...]和[速度1, 速度2, 速度3, 速度4...]当 CPU 想要第一颗子弹的“位置”时,搬运工搬回来的 64 字节箱子里,密密麻麻塞满了十几颗子弹的位置数据!
CPU 算完第一颗,伸手一拿,第二颗、第三颗的数据已经在手边(L1 Cache 里)了,不需要再去仓库拿。CPU 可以像机关枪一样不间断地连续计算(这就是 Cache Hit,缓存命中率 > 95%)。
3. 总结归纳
“利用连续内存布局最大化空间局部性”的意思就是:
你刻意改变了数据的存储结构,把 CPU 在这一帧(比如 Tick 位置)会集中使用的数据,紧紧凑凑地挨在一起放在内存里。这样 CPU 每次读取内存(拉取一个 Cache Line)时,带回来的全都是马上要用的有效热数据,没有任何空间浪费,从而将内存访问的延迟降到了最低。
这就是 DOD(面向数据设计)比 OOP(面向对象设计)在海量实体运算时快上几十倍的根本原因。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)