中EN
概念页

计算机硬件与内存层次

Computer Hardware & Memory Hierarchy

更新于 2026-10-13 约 8 分钟CPU指令集虚拟内存TLB缓存NUMAGEA

从冯·诺依曼结构讲起,依次是 CPU 的组成(ALU、VPU、寄存器、控制单元、时钟)、总线与字长、机器语言、汇编和寻址方式,再到内存体系:内存映射、虚拟内存和 TLB,以及为了缩小“内存鸿沟”而设计的寄存器文件、多级缓存(缓存行、标签、组相联、替换和写策略、一致性协议)和 NUMA(PS3 的 SPU 本地存储、PS2 的 scratchpad)。对游戏程序员最重要的结论是:数据要连续、紧凑、顺序访问;热循环的代码要小。

CPU 的组成(卷 I 3.4.2–3.4.4)

  • 冯·诺依曼结构:CPU 和内存通过总线相连,外设通过 I/O 端口或扩展槽连接。
  • CPU 包含:ALU(整数运算和移位)、VPU(向量单元,即 SIMD)、内存控制器/MMU、寄存器组、控制单元 CU(取指、译码、分派;在流水线和超标量 CPU 中还负责分支预测和乱序调度)。如今的 CPU 实际上已没有独立的 FPU,标量浮点运算也由 VPU 完成,省下的晶体管用于更大的缓存和乱序逻辑。整数与浮点之间的转换已不像 FPU 时代那样昂贵,但仍应尽量避免。
  • 寄存器用多端口 SRAM 实现,有名字而没有地址。除通用寄存器外,还有专用寄存器:指令指针 IP、栈指针 SP、基址指针 BP、状态寄存器(Z、C 等标志)。FPU 和 VPU 的寄存器更宽,比如 SSE2 的 128 位。
  • 时钟:一条指令不一定只需一个周期。有的指令由多个 µ-op 组成;N 级流水线的单条指令延迟至少是 N 个周期,但每周期可以完成一条。所以 MIPS 和 FLOPS 只是平均值,主频不能直接换算成算力,只能靠基准测试来衡量。书中的例子是:3 GHz 的串行 CPU,如果一次浮点乘法平均要 6 个周期,理论上约为 0.5 GFLOPS。

内存、总线与字(卷 I 3.4.5–3.4.6)

ROM 与 RAM;SRAM 与 DRAM(DRAM 由电容构成,需要定期刷新,读取会破坏数据);多端口、SDRAM、DDR。地址总线宽度决定可寻址空间:16 位是 64 KiB,32 位是 4 GiB,64 位是 16 EiB。数据总线宽度决定每次能传输多少;读取比总线窄的数据仍要读完整的宽度,再掩码移位。“字 (word)”和“n 位机”的含义都依语境而定,例如 8088 内部是 16 位,但数据总线只有 8 位。

机器语言与汇编(卷 I 3.4.7)

ISA(指令集架构)与 ABI(调用约定等更高层的协议)不同。常见指令类别有:移动(load/store)、算术、位运算、移位/循环移位、比较(做减法只设标志位)、跳转与分支、压栈与出栈、调用与返回、中断、NOP。指令由操作码、操作数和选项(寻址方式)组成;RISC 多为定长编码,CISC 多为变长编码,VLIW 则把多个操作打包进一条很宽的指令。寻址方式有寄存器、立即数、直接、寄存器间接(即指针解引用)、相对(即数组下标访问)等。书中用 x86 汇编示范了 if (a > b) return a + b; else return 0;(cmp、jle、add、xor eax, eax)。

内存映射与虚拟内存(卷 I 3.5.1–3.5.2)

  • 内存映射:把地址空间的不同区段映射到 ROM、RAM、内存映射 I/O 设备或显存(VRAM);另一种方式是端口映射 I/O。PS4 和 Xbox One 上 CPU 与 GPU 共享统一内存;PC 的显存在显卡上,通过 PCIe 与主存往来,这道物理隔阂正是图形 API 复杂的主要原因之一。
  • 虚拟内存:程序使用虚拟地址,CPU 借助操作系统维护的页表把它重映射到物理页、磁盘交换文件,或者标记为未映射。这样既能使用多于物理内存的空间,也隔离了各个进程。页大小是 2 的幂,通常为 4 或 8 KiB。以 4 KiB 页为例,低 12 位是页内偏移,高 20 位是页号;MMU 查页表把虚拟页号换成物理页号,再与偏移拼起来。
  • 缺页 (page fault):页表显示该页未映射到物理内存时,MMU 触发中断通知操作系统。访问未分配的页通常会崩溃并生成转储;访问被换出的页时,操作系统挂起程序、从交换文件读回该页,然后继续执行,程序本身察觉不到。通常只在内存压力大时才换出页,且尽量只换出最不常用的页,以免程序在内存页与磁盘页之间反复“颠簸 (thrashing)”。
  • TLB:页表很大,所以在 MMU 内用一块小缓存保存最近用过的映射,其原理与普通缓存相同。

内存鸿沟与缓存层次(卷 I 3.5.3–3.5.4)

访问延迟取决于存储技术(SRAM 比 DRAM 快但更贵)、端口数和物理距离。8086 时代访问内存约 4 个周期,与执行指令相当;如今 Core i7 上寄存器指令只需 1–10 个周期,访问主存却要约 500 个周期,这就是内存鸿沟 (memory gap)。应对方法有三类:把更小更快的存储放在离核心更近的地方(缓存);在等待内存时让 CPU 做别的事(延迟隐藏);合理组织数据以减少访存。后两者见卷 I 第 4 章。

下图是书中图 3.28 的双核示意,延迟数值为书中给出的近似值:

flowchart LR
  C0["核心 0"] --> L10["L1(约 4 周期)"]
  C1["核心 1"] --> L11["L1(约 4 周期)"]
  L10 --> L2["共享 L2(约 25 周期)"]
  L11 --> L2
  L2 --> RAM["主存(150+ 周期)"]

越往右容量越大、延迟越高;命中率与延迟之间存在根本的权衡。注意书中两处数字口径不同:3.5.3.1 正文说 Core i7 访问主存“约 500 个周期”,图 3.28 标注“150+ 周期”,都是数量级上的近似,具体取决于硬件。

  • 局部性分为空间局部性和时间局部性,因此缓存以连续的缓存行 (cache line) 为单位搬运数据。
  • 地址拆分:设缓存共 2M2^M 字节、行大小 2n2^n 字节,主存地址的低 nn 位是行内偏移,接下来的 M−nM-n 位是行索引,其余高位是标签 (tag),用来标记这一行来自主存中哪个缓存大小的块。以 32 KiB 缓存、128 字节行为例,共 256 行,主存地址 0x203 落在第 ⌊515/128⌋=4\lfloor 515/128 \rfloor = 4 行。
  • 直接映射容易出现两个无关的块互相“乒乓”驱逐的病态情况;N 路组相联让每个地址可以映射到 N 行之一,路数越多通常越好。替换策略有 NMRU(不驱逐最近使用的那一路)、FIFO(直接映射时只能如此)、LRU、LFU、伪随机。
  • 多级缓存:L1 小而快,L2 更大更慢,有的 PC 还有 L3、L4。L1 一定把指令缓存 I∗∗和∗∗数据缓存D** 和**数据缓存 D 分开,更高层的缓存一般不分。
  • 写策略:写直达 (write-through) 每次写都同步到主存;写回 (write-back) 只在脏行被驱逐或显式刷新时才写回主存。
  • 一致性:多核时要保证各核的缓存与主存“看起来”一致,常见协议有 MESI、MOESI、MESIF(第 4 章详述 MESI)。
  • 避免缓存未命中:对 D,数据要连续、尽量小、顺序访问;对I,数据要连续、尽量小、顺序访问;对 I,热循环的代码要尽量小,内层循环少调用函数,内联要适度,因为过度内联会让代码膨胀,装不进缓存。

UMA 与 NUMA(卷 I 3.5.5)

  • UMA:所有核心看到同一块主存,靠缓存层次缓解延迟,但核心会争用共享缓存和主存。例如 PS4 有两个各 4 核的簇,每簇共享一个 L2。
  • NUMA:每个核心有一块显式访问的本地存储 (local store),与主存之间通过 DMA 传输。PS3 是典型:PPU 独占 256 MiB 主存,RSX 独占 256 MiB 显存,每个 SPU 有 256 KiB 本地存储,各地址空间完全隔离(8 个 SPU 中只有 6 个可供游戏使用)。
  • PS2 scratchpad:EE 芯片上有 16 KiB 内存映射、不经缓存的区域。它的主要好处不在于低延迟,而在于访问时不占用系统总线:DMA 在主存和 VU 之间传输数据时,EE 可以同时在 scratchpad 上计算。

与其他来源的关系

GPP 的 数据局部性 从模式的角度给出了同样的结论(连续数组、冷热分离、避免指针追逐),GEA 则从硬件机制上解释了原因,两者一致。GPU 一侧的显存与带宽问题见 RTR4 的 GPU 内存、缓冲压缩与多重缓冲。

  1. Game_Engine_Architecture_4Ed_-_Jason_Gregory.pdf
  2. Jason Gregory《Game Engine Architecture》第 4 版(2026)卷 I 第 3 章 3.4–3.5 节

相关页面

游戏开发C++ 内存布局与对齐C++ Memory Layout & Alignment理解 C/C++ 程序在内存中的样子,是写高性能引擎代码的前提。主要内容包括:声明与定义、内部与外部链接;可执行映像的几个段(text、data、BSS、rodata)以及栈和堆;结构体成员的自然对齐和填充;继承和虚函数如何改变类的布局(vpointer 和 vtable)。GEA 也简要回顾了面向对象:多重继承容易产生致命菱形,所以通常只允许 mix-in;组合往往比继承更好。游戏数学数值表示与浮点数Numeric Representation & Floating Point游戏引擎里到处是数值,所以要清楚它们在机器中如何存储:二进制和十六进制、补码整数、定点数、IEEE-754 浮点数(量级与精度此消彼长、次正规数、机器 epsilon、ULP),以及多字节值的大小端问题和安全的类型双关 (type punning) 方法。书中一个直观的例子是:用 32 位浮点数记录以秒为单位的游戏时钟,大约 12.14 天后,加 1/30 秒就不再改变它的值。游戏开发数据局部性Data Locality数据局部性 (Data Locality) 是一种优化思路:CPU 读取内存时会把相邻的一整条缓存行一起载入,所以把数据按处理顺序连续存放,就能大幅减少缓存未命中。具体手段包括:组件按类型存进连续数组,不再到处跟随指针;把活跃对象排在前面;把冷热数据拆开。代价是要放弃一部分继承、接口与指针带来的抽象。图形学GPU 内存、缓冲压缩与多重缓冲GPU Memory, Buffer Compression & Multiple Buffering带宽是稀缺资源。GPU 通过独立显存或统一内存架构 (UMA)、多级缓存与按瓦片存储提高局部性,并用基于瓦片表的无损压缩和快速清除减少渲染目标的带宽(注意:无损压缩只省带宽,不省内存)。颜色缓冲有高彩、真彩、深色三种模式;显示控制器以固定刷新率或 FreeSync/G-sync 自适应同步把颜色缓冲送到显示器;单/双/三缓冲在撕裂、等待与延迟之间取舍。游戏开发多线程渲染与任务系统Multithreaded Rendering & Task SystemsCPU 时钟频率约在 2003 年停在 3.4 GHz 附近后,性能来自多核。应用阶段的优化一是让代码与内存访问更缓存友好,二是利用多核:多处理器流水线(时间并行,提高吞吐但增加延迟)、并行处理(空间并行,延迟更低)以及两者结合的任务系统;图形 API 用命令缓冲让多个线程并行录制绘制命令,新一代 API(DirectX 12、Vulkan、Metal)把更多责任交给应用以降低驱动开销。游戏开发并发与并行基础Concurrency & Parallelism FundamentalsGEA 区分两个正交的概念:并发 (Concurrency) 是软件层面多个控制流读写共享数据,核心难题是数据竞争;并行 (Parallelism) 是硬件层面两个以上部件同时工作。硬件并行又分为提升单线程速度的隐式并行(流水线、超标量、VLIW,即指令级并行 ILP)和为同时运行多条指令流而设计的显式并行(超线程、多核、多处理器、集群/云)。游戏开发进程、线程与纤程Processes, Threads & Fibers并发编程依赖操作系统提供的服务:内核态与用户态、中断、系统调用、抢占式多任务。进程只是为线程提供运行环境(虚拟地址空间与资源),线程才是操作系统的基本执行单位(调用栈 + 寄存器)。线程由内核抢占调度;纤程 (Fiber) 由程序协作调度,适合实现引擎任务系统;用户级线程与协程完全在用户空间切换上下文,更轻量。游戏开发无锁编程与内存序Lock-Free Programming & Memory Ordering「无锁 (lock-free)」的真正含义不是不用互斥锁,而是从不让线程阻塞(作者说叫 block-free 更贴切)。数据竞争有三个来源:关键操作被打断、编译器与 CPU 的指令重排、缓存一致性协议优化造成的内存序问题。对应的工具是原子指令(TAS、XCHG、CAS、LL/SC)和内存屏障/栅栏(acquire、release、full fence),C++11 用 std::atomic 与 std::memoryorder 把它们封装成可移植接口。在此基础上可以写出自旋锁、可重入锁、读写锁,以及基于「本地准备 + 一次原子提交」事务的无锁数据结构。游戏开发自定义内存分配器Custom Memory Allocators内存从两方面影响性能:动态分配(malloc / 全局 new)很慢,以及访问模式主导现代 CPU 的性能。GEA 的经验法则是「尽量少做堆分配,绝不在紧密循环里做堆分配」,并介绍游戏引擎常用的自定义分配器:栈分配器(含双端栈)、池分配器、对齐分配、单帧与双缓冲分配器;以及如何用栈/池避免碎片,或用碎片整理加指针重定位(智能指针、句柄)解决碎片。游戏开发容器与哈希表Containers & Hash Tables容器的职责都是容纳和管理零个或多个元素,差别在于实现方式与取舍。GEA 概览了常见容器与操作、迭代器、大 O 复杂度与内存布局的综合考量,讨论引擎为何常自研容器、C++ 标准库 / Boost / Folly / Loki 的利弊,并重点讲了动态数组的增长策略与哈希表(开放哈希与封闭哈希、哈希函数、线性/二次探测、Robin Hood 哈希)。游戏开发游戏时间线与时钟Game Timelines & Clocks游戏引擎要同时处理多种「时间」:真实时间、游戏时间、动画片段的局部时间、CPU 周期等。GEA 用抽象时间线 (abstract timeline) 统一看待它们,并讨论帧时间 \Delta t 的测量方法、高精度计时器、时间单位与时钟变量类型的取舍,以及断点恢复后的异常 \Delta t 处理。(卷 I 第 8 章 引言、8.4–8.5 节)图形学图形栈与渲染引擎架构Graphics Stack & Rendering Engine Architecture渲染引擎要解决的"渲染问题"是:给定由场景元素、虚拟光源和虚拟相机组成的虚拟世界,生成相机看到的二维彩色图像。GEA 把它拆成可见性问题和光照问题,按解法把引擎分为光线追踪、深度缓冲光栅化、辐射度和混合四类;光栅化引擎通常采用立即模式的两层结构:下层是图形管线,上层是负责剔除、提交几何和设置着色器参数的应用层。本页讲这套整体架构,管线内部见 渲染管线。综合游戏中的内存管理Memory Management in Games本页把 wiki 中关于内存的内容串成一条线:内存从两方面影响游戏,一是访问模式(缓存未命中远比计算昂贵),二是分配本身(通用堆分配慢且产生碎片,而主机上没有自动换页的虚拟内存兜底)。对策按生命周期组织:常驻与关卡数据用栈、每帧临时数据用单帧/双缓冲分配器、同尺寸小对象用池或小内存分配器、大小不一的动态对象用堆加碎片整理与句柄;资源按引用计数和分块加载管理,流送系统把世界切成等大的块;UE 则在 UObject 层用垃圾回收管理对象生命周期。最后,没有准确的追踪工具,以上一切都无从验证。各结论的细节与出处见所链接的概念页。

输入关键词开始搜索

滚轮或双指缩放 · 拖动平移 · 双击放大 / 还原