Menu Close

CPU 如何与内存通信:Load、Store、地址与内存总线

CPU 可以执行算术运算、比较数值、执行分支,也可以进行各种位操作。

CPU 如何与内存通信:Load、Store、地址与内存总线

但是,大多数真正有用的程序,仅靠寄存器和 ALU 是远远不够的。

程序还需要大量数据。

这些数据可能包括变量、数组、程序指令、图像、正在处理的文件、网络缓冲区,以及许多其他类型的信息。

绝大多数信息都不可能一直保存在 CPU 寄存器中。

它们必须存放在其他地方。

这就是内存存在的意义。

CPU 会不断与内存通信,从内存中读取数据,也会把计算结果重新写回内存。

在指令层面,其中最重要的两类操作就是:

Load,加载

以及

Store,存储

Load 把数据从内存送向 CPU

Store 则把数据从 CPU 写向内存。

看起来只是两种简单的操作,但它们背后实际上涉及地址、缓存、总线、片上互连、内存控制器和 DRAM 等一整套硬件系统。

理解这条数据路径,是理解 CPU 如何连接整个计算机系统的重要一步。

1. 寄存器很快,但容量很小

CPU 内部包含寄存器,用来保存当前指令正在使用的数据。

例如,假设程序需要把两个数字相加。

CPU 可能执行一条类似下面这样的指令:


它的含义大致是:


因为两个输入数据都已经位于 CPU 内部的寄存器中,所以 ALU 可以非常快速地完成这个加法。

但是,寄存器数量非常有限。

不同指令集架构可能拥有几十个,甚至上百个架构可见寄存器。

而一个程序可能需要处理几 MB、几 GB,甚至更多的数据。

显然,这些信息不可能全部装进寄存器。

因此,大多数程序数据最终都需要存放在内存中。

CPU 在需要这些数据时,再把它们从内存取到寄存器;计算完成后,也可能再把结果写回内存。

2. 内存通过地址进行组织

主内存可以简单理解成一个巨大的存储空间,其中包含大量存储位置。

每一个位置都有自己的地址。

例如,一个非常简化的内存可以表示成:


CPU 通常不会对内存说:

“把变量 counter 给我。”

在硬件层面,CPU 使用的是地址。

CPU 真正发出的请求更接近:


或者:


因此,地址是连接软件世界和硬件世界最基础的机制之一。

3. Load 从内存读取数据

Load 指令的作用,是把数据从内存传送到 CPU 寄存器。

例如:


从概念上来说,它表示:


如果内存地址 0x1000 中保存的值是:


那么执行 Load 之后:


CPU 接下来就可以使用这个值进行算术、比较或其他运算。

不过,在真正的处理器中,内存地址通常不是直接完整地写在指令里的,而是通过寄存器和偏移量计算出来。

例如,一条 MIPS 风格的 Load 指令可能是:


CPU 首先计算地址:


然后读取这个地址中的一个 word,并把它放入 $t0

因此,一条 Load 指令通常包含两个重要步骤:

地址计算

以及

内存访问

4. Store 把数据写入内存

Store 指令执行的方向正好相反。

它会把 CPU 寄存器中的数据写入内存。

例如:


概念上表示:


如果 r1 中保存的是:


CPU 就会把 42 写入指定的内存地址。

一条 MIPS 风格的 Store 指令可能是:


CPU 同样先计算:


然后把 $t0 中的数据写入这个地址。

因此,Load 和 Store 就构成了 CPU 寄存器与内存之间最基本的数据桥梁:

5. ALU 经常负责计算内存地址

ALU 并不仅仅用来执行加法、减法之类的算术运算。

在内存访问过程中,ALU 也承担着非常重要的作用。

例如:


假设:


CPU 就必须计算:


结果为:


这个结果就是实际要访问的内存地址。

因此,数据通路可以简单理解为:


这也解释了为什么 Load 和 Store 指令同样需要经过 ALU。

虽然这些指令的主要目标并不是执行普通算术计算,但 CPU 仍然需要 ALU 来计算有效地址,也就是 Effective Address

6. 地址和数据是两种不同的信息

CPU 与内存通信时,需要传输不同类型的信息。

第一种是:

Address,地址

它告诉内存系统:

CPU 想访问哪里。

第二种是:

Data,数据

它才是真正被读取或写入的信息。

对于 Load:


CPU 把地址送给内存,然后从内存得到数据。

对于 Store:


CPU 不但发送地址,还要把准备写入的数据一起发送出去。

除此之外,CPU 还必须告诉硬件:

这次操作究竟是读,还是写。

所以,CPU 与内存通信通常可以归纳为三个最基本的概念:


传统计算机体系结构中,它们通常对应:


也就是:

地址总线

数据总线

控制总线

7. 什么是内存总线?

传统意义上的 Bus,总线,可以理解为一组共享的电气连接,用于在不同硬件组件之间传递信号。

一个非常简化的 CPU 与内存系统可以表示成:


地址信号负责指出 CPU 想访问哪个位置。

数据信号负责传输真正的内容。

控制信号则告诉系统当前要执行什么操作。

例如:


也就是读和写。

在早期计算机架构中,这些总线往往表现得更加直接和明显。

但现代处理器已经复杂得多。

今天的 CPU 内部可能包含:

多级缓存、集成式内存控制器、高速片上互连,以及专用的内存通道。

所以,“内存总线”仍然是学习计算机体系结构时非常重要的基础概念。

但现代 CPU 并不一定真的存在一条简单的共享总线,直接把 CPU 核心和 DRAM 连在一起。

8. CPU 通常不会直接与 DRAM 通信

最简单的计算机结构图经常画成:


这非常适合入门学习。

但真正的现代计算机通常并不是这样。

CPU 核心与主内存之间,实际上存在很多层结构。

更接近现实的路径大致是:


CPU 执行一条 Load 指令时,它通常不会立刻跑到 DRAM 中寻找数据。

处理器首先会检查:

需要的数据是不是已经存在于 Cache,也就是缓存中。

如果数据已经在缓存中,CPU 就可以非常快地获得它。

只有当缓存中找不到所需数据时,请求才需要继续向更低层传递,最终有可能访问 DRAM

9. 为什么 Cache 如此重要?

现代 CPU 执行指令的速度非常快。

而主内存相对慢得多。

如果每一条 Load 指令都必须等待 DRAM 返回数据,那么 CPU 大量时间都会浪费在等待上。

Cache 就是为了解决这个问题。

缓存会把最近使用过的数据,或者很可能马上会使用的数据,暂时保存到距离 CPU 更近的位置。

典型的存储层次结构可以表示成:


一般来说:

越接近 CPU,速度越快。

但是容量也越小。

如果 CPU 请求的数据正好可以在缓存中找到,这叫做:

Cache Hit,缓存命中

如果找不到,则称为:

Cache Miss,缓存未命中

出现 Cache Miss 后,请求就需要继续访问下一层缓存。

如果所有缓存都没有,最终才会访问主内存。

10. 内存通常以数据块为单位传输

CPU 有时候可能只需要几个字节。

但现代内存系统通常不会每次只从 DRAM 中取一个孤立的字节。

Cache 会把内存组织成一个个数据块。

这些块通常称为:

Cache Line,缓存行

在很多现代处理器中,一个常见的 Cache Line 大小是:

64 字节

当然,不同处理器架构可能有所不同。

假设 CPU 想读取地址:


内存系统可能并不会只读取这个地址对应的几个字节。

它可能会把包含 0x1008 在内的整个 Cache Line 一次性加载进缓存。

这样做非常有用,因为程序通常会连续访问附近的数据。

例如:


程序读取完 array[0] 之后,很可能马上会读取:


所以,把附近的数据一起装进 Cache,往往可以提高后续访问速度。

这种现象称为:

Spatial Locality,空间局部性

11. 内存控制器负责与 DRAM 通信

如果 CPU 请求的数据无法在 Cache 中找到,请求最终就会到达:

Memory Controller,内存控制器

现代 CPU 通常会把内存控制器直接集成在处理器内部,或者集成到处理器芯片中。

内存控制器负责把 CPU 发出的内存请求转换成 DRAM 可以理解的命令。

DRAM 本身并不是一整块简单的线性存储空间。

从硬件组织角度看,它通常还包括:


也就是:

通道、Rank、Bank、行和列。

内存控制器负责决定如何访问这些结构,并处理 DRAM 所要求的各种时序。

因此,CPU 核心本身并不会直接控制某一个 DRAM Row 或 Column。

CPU 只需要提出内存请求。

更底层的 DRAM 通信工作,则交给内存控制器完成。

12. 一条 Load 指令到底发生了什么?

现在我们可以把一条 Load 指令完整地跟踪一遍。

假设 CPU 执行:


一个简化的执行过程可能是:


从概念上看:


所以,一条看起来非常简单的 Load 指令,背后实际上可能触发大量硬件活动。

13. 一条 Store 指令到底发生了什么?

Store 的过程与 Load 类似,但数据传输方向不同。

假设:


CPU 必须确定两件事情:


以及:


一个简化过程大致是:


现代 CPU 经常使用一种技术:

Write-Back Cache,回写式缓存

使用 Write-Back Cache 时,一条 Store 指令执行后,并不一定马上把数据写进 DRAM

CPU 可以先修改 Cache 中的副本。

被修改过的 Cache Line 会被标记为:

Dirty,脏数据

等到之后需要替换这个 Cache Line,或者系统认为应该同步时,再把数据写回更低层缓存或者 DRAM

这种机制可以避免每执行一条 Store 指令,CPU 都必须停下来等待速度较慢的 DRAM

14. Load 和 Store 把程序连接到内存

考虑一条非常简单的 C 语言代码:


从程序员角度看,它就是一个加法。

但假设:

a

b

c

都保存在内存中。

处理器内部可能需要执行类似这样的操作:


当然,实际生成什么指令,还取决于:

编译器、处理器架构、优化级别、寄存器分配,以及数据是否已经存在于 Cache 或寄存器中。

但最重要的基本模式是不变的:


这是一台计算机内部最重要的数据流之一。

程序中的数据通过 Load 进入 CPU

CPU 使用寄存器和 ALU 进行计算。

然后通过 Store 把结果重新送回内存。

15. 内存访问的代价远高于寄存器访问

从程序员角度看,读取一个变量似乎只是一件很简单的事情。

但是,从 CPU 的角度看,访问这个变量的成本可能完全不同。

如果数据已经在寄存器中,那么执行单元几乎可以立即使用。

如果数据在 L1 Cache 中,需要稍微多一点时间。

如果在 L2 或 L3 Cache 中,则需要更长时间。

如果最终必须访问 DRAM,那么代价就会明显增加。

因此,两条看起来几乎完全相同的 Load 指令:


实际执行时间可能差别很大。

原因就在于:

一个地址的数据可能正好位于 L1 Cache。

另一个地址的数据可能必须从 DRAM 中读取。

这就是为什么内存访问模式对程序性能有如此巨大的影响。

现代 CPU 会投入大量芯片面积和工程设计,用于:


真正困难的问题并不仅仅是:

如何让 CPU 算得更快。

还有另一个同样重要的问题:

如何不断给 CPU 提供足够快的数据。

16. 简单总线模型与现代 CPU

学习计算机体系结构时,一个非常好的起点是下面这个简单模型:


这个模型非常清楚地解释了几个基本概念。

CPU 选择一个地址。

控制信号说明执行 Read 还是 Write。

数据则在 CPU 和内存之间传输。

但是,现代 CPU 在这套基础逻辑之上增加了许多层结构:


今天的内存系统已经比早期计算机复杂得多。

但是最核心的原则并没有改变。

CPU 必须:

确定数据在哪里。

提出正确的读写请求。

然后通过内存系统完成数据传输。

发生变化的只是:

实现这件事情的硬件越来越复杂,也越来越高效。

17. 总结

CPU 不能只依靠寄存器完成真正有用的计算。

程序中包含的数据远远超过寄存器能够保存的数量。

因此,CPU 必须不断和内存通信。

Load 把数据从内存送到 CPU 寄存器。

Store 把数据从 CPU 寄存器送回内存。

CPU 首先计算地址。

内存系统找到对应的位置。

然后数据通过整个内存层次结构进行传输。

在最简单的模型中,这种通信可以理解成:


也就是传统意义上的:


现代计算机又在 CPU 核心和物理内存之间加入了:

Cache、Load/Store Unit、片上互连、Memory Controller,以及 DRAM Channel 等大量结构。

但是最根本的逻辑仍然很简单:

CPU 负责计算。

内存负责保存程序更大的工作状态。

而 Load 和 Store,把两者连接在一起。

理解了 CPU 如何与内存通信之后,我们就可以继续向计算机系统的更高一层前进。

接下来的问题已经不再只是:

硬件如何访问数据?

而是:

究竟是谁负责管理 CPU、内存、设备、文件和正在运行的程序?

答案就是:

操作系统

Posted in 计算机结构教程