Loading

Loading

GPU systems学习笔记(二)存储器层次结构

每个程序员都应该了解的关于内存的知识(《What Every Programmer Should Know About Memory》)

内存基础

CPU的缓存通常采用SRAM来实现,主存通常采用DRAM来实现。长期来看,CPU的执行速度提升快于DRAM访问延迟的改善,二者之间的差距不断扩大,因此存储系统逐渐成为性能瓶颈。这对大容量存储和存储子系统尤其如此,这些子系统因容量、成本等原因,访问速度的提升较其他组件慢。

image

实际上,磁盘和SSD等持久化存储设备的访问速度更慢。操作系统会使用主存缓存磁盘或SSD中的文件数据;当主存不足时,也可以把磁盘或SSD上的交换空间作为主存的后备存储,但其速度远低于真正的主存。

那么我们有什么方法来绕过大容量存储的缓慢问题呢?

常用的手段包括改进RAM与内存控制器、使用多级CPU Cache,以及使用DMA减少数据传输过程中CPU的参与。DMA主要降低CPU搬运数据的开销,本身并不能消除底层存储的延迟或带宽限制。

image

传统的通用硬件

在本文写作时所讨论的传统个人计算机和小型服务器中,常见芯片组由北桥和南桥两部分组成。现代CPU或SoC通常已经把内存控制器等功能集成到处理器内部。

在这种传统架构中,CPU通过公共总线(前端总线,FSB)连接到北桥。北桥包含内存控制器等功能,其实现决定了系统支持的内存接口和规格。南桥主要连接较低速的I/O设备和接口。不同的DRAM接口和代际,如RDRAM、SDRAM及后续DDR SDRAM,需要相应的内存控制器支持。

image

如果为多个内存通道或内存模块配置可并行工作的内存控制器,就可以增加可同时处理的内存请求,从而提高聚合带宽;此时北桥或处理器互连本身也可能成为新的瓶颈。

image

历史继续演进,出现了把内存控制器集成到CPU中、并让不同CPU连接各自本地内存的架构。由于一个CPU访问本地内存和其他CPU所连接的远端内存具有不同的延迟和带宽,这种多节点架构称为NUMA(非一致内存访问)。仅仅把内存控制器集成到CPU中,并不必然构成NUMA。

这样的架构中,CPU访问其他节点上的远端内存时,可能需要经过一个或多个处理器互连跳数,所以会产生额外成本。具体路径由系统的互连拓扑决定,例如图中某些节点之间可能需要经过中间节点。(嗯...怎么有图论的影子呢?)

image

随机存取存储器类型 RAM Type

“为什么同一台机器里会有不同类型的内存”

更具体地说,为什么既有 静态 RAM(SRAM)又存在 动态 RAM(DRAM)?

前者速度更快,功能也相同。为什么机器的 SRAM 里没有所有内存?

SRAM 的生产和使用成本远高于 DRAM。

所以计算机采用分层设计:容量小、速度快的CPU Cache通常使用SRAM,容量更大的主存通常使用DRAM。它们位于存储器层次结构的不同层级,并不是把同一块主存简单划成SRAM和DRAM两部分。

让我们来看看 1bit SRAM是怎么构造的:

image

一个典型的SRAM存储单元由6个晶体管构成。只要持续供电,其状态就能保持,不需要像DRAM那样周期性刷新。

相比之下,典型的1 bit DRAM存储单元由一个电容和一个晶体管构成。SRAM每个bit需要更多晶体管、单元面积更大、集成密度更低,因此单位容量成本远高于DRAM。DRAM中的电容会漏电,所以数据需要周期性刷新;而且读取通常会破坏电容中的电荷,需要由感应放大器恢复数据。

image

那么DRAM多久需要刷新呢?许多DRAM规定所有行必须在约64 ms的刷新窗口内各自完成一次刷新;具体窗口和刷新间隔会随器件标准、容量与温度而变化。64 ms是时间间隔,不是频率。

image

在内存中,DRAM的排列方式是这样的,每个单元代表一个bit的存储。

横向导线是字线,用于选中一行存储单元;纵向导线是位线,用于把各单元连接到感应放大器。图中的RAS#、CAS#等外部命令信号通常用“#”表示低电平有效,但字线和位线本身不能简单理解为两个低电平有效的行列选通信号。

访问时先激活一行,使该行数据进入感应放大器组成的行缓冲;随后通过列命令选择行缓冲中的一部分数据进行传输。实际一次读写通常传输一组数据,而不是逐个扫描单个bit。

image

(这个设计看起来很简单,但实际实现起来还是很复杂的,涉及了非常多的细节)

SDRAM(Synchronous DRAM)

同步DRAM的命令和数据传输由时钟同步协调,内存控制器在规定的时钟边沿发出命令并遵守相应时序约束。

图中的RAS和CAS带“-”,表示这些信号低电平有效。激活行(ACTIVATE)后,需要等待行到列延迟 \(t_{RCD}\),才能发出READ/WRITE等列命令;发出READ命令后,再经过CAS Latency(\(CL\))才开始返回数据。行地址和列地址复用同一组地址引脚可以减少引脚数量,但 \(t_{RCD}\) 是DRAM内部激活行所需的时序约束,不能简单等同于时分复用。

image

在激活某个bank中的新行之前,如果该bank当前已有另一行处于打开状态,就需要先发出PRECHARGE命令关闭当前行,并等待 \(t_{RP}\);随后发出ACTIVATE命令打开新行,再等待 \(t_{RCD}\) 后进行列访问。如果连续访问当前已经打开的同一行(row hit),则不需要每次都先预充电。图中WE#、RAS#等信号的组合编码出PRECHARGE等命令,不能把它们单独理解为充电曲线。

image

所以这也教会了我们怎么读DDR的芯片手册,应该重点关注这些性能。

image

内存类型 memory type

早期的同步DRAM采用单倍数据速率(SDR),后来引入了DDR SDRAM(双倍数据速率同步DRAM)。DDR在时钟上升沿和下降沿都传输数据,因此在相同I/O时钟频率下,理论数据传输率可以达到SDR的两倍。

第一代DDR采用2n预取:内部存储阵列一次预取两个数据字,再由I/O接口在两个时钟边沿依次传出。这里需要区分DRAM核心阵列频率、I/O时钟频率和有效数据传输率,不能笼统地说“频率没有变化”。

image

DDR2把预取宽度提高到4n,并提高I/O数据传输率;后续代际还通过更宽的预取、更多bank、更高I/O速率以及信令和功耗改进来提升性能。各代并不是把所有频率和“通路数”简单翻倍。

CPU缓存(cache)

每种存储方式访问耗时总览:

image

也就是说如果我们能把访问限制在cache中,那么带来的效率或性能上的差异还是非常显著的。

缓存常常利用时间局部性和空间局部性,像局部变量和循环,不停访问同一块连续的空间,提高访问效率。

程序员需要帮助处理器来充分利用缓存的资源。

让我们来看看早期的CPU缓存的示意图。

image

现代处理器通常采用改进型 Harvard architecture(改进型哈佛结构):一级指令缓存和一级数据缓存分离,使取指和数据访问可以同时进行;更低层的缓存和主存通常仍由指令与数据共享。它有利于流水线运行,但不等同于流水线设计。所以现代缓存的架构常常长这样,L1 指令缓存和 L1 数据缓存是分开的,如 L1i cache、L1d cache:

image

对于多核的处理器,如intel core i7 ,内部架构如图:

(core0~core3代表这个处理器有4个核)

在这张图所示的 Core i7 中,每个核有自己的寄存器、分离的一级数据缓存和一级指令缓存,以及各自私有的、由指令和数据共用的二级缓存。四个核共享三级缓存。

缓存以缓存行(cache line)为单位在不同层级之间传输和管理数据;处理器仍然可以按字节、字等粒度访问缓存行中的数据。

image

缓存在CPU内部存储的方式

一个缓存行包含一个数据块,并配有 tag、valid bit 等元数据;采用写回策略的可写缓存通常还需要 dirty bit。访问缓存时,地址通常被拆分为 tag(标记)、set index(组索引)和 block offset(块内偏移);set 和 offset 是缓存组织及地址划分中的概念,并不是缓存行内存放的三个组成部分。

image

CPU缓存的具体实现

1.Fully Associative Cache 全相联缓存

全相联缓存只有一个 set,地址中没有 set index,只剩 tag 和 block offset。任意内存块都可以放入任意缓存行;查找时需要把目标 tag 与所有有效缓存行的 tag 并行比较,因此硬件成本较高。

虽然成本很高但仍有使用场景,例如容量较小的 TLB 可能采用全相联结构;较大的 TLB 也常采用组相联结构。

image

2.Direct-Mapped Cache 直接映射的缓存

硬件实现比较简单:set index 直接选中唯一一条缓存行,然后比较该行的 tag,以判断是否命中。

image

3.Set-Associative Cache 组相联缓存

相当于上述二者的折中,先去选择在哪一个集合里面,然后再在这个集合里面去进行一些并行的比较。这样成本会小很多,现在大部分的缓存都是这个形式的。

image

Cache的大小和关联性对性能的影响

一般来讲,缓存越大,不命中的概率就小一些

image

缓存的读写

  • 写通:写入缓存的同时,也把数据写到下一层存储结构。下一层可能是下一级缓存,并不一定每次都直接写到主存;写缓冲区也常用于隐藏写入延迟。

  • 写回:缓存被改写后不会立即更新下一层存储结构,而是把该缓存行标记为脏。当这一行被逐出缓存时,如果脏位有效,就将整条缓存行写回下一层。相当于一种延时操作(有点像线段树的 lazy tag)。

    在多核、多处理器环境中,同一内存块可能同时存在于多个核心的缓存中,因此无论采用写通还是写回,都需要缓存一致性机制。写回策略还需要一致性协议追踪哪些缓存持有最新数据。

  • MESI:一种缓存一致性协议。是 Modified、Exclusive、Shared、Invalid(缓存行可能处于的四种状态)的缩写。

image

虚拟内存

虚拟内存为每个进程提供独立的虚拟地址空间。操作系统在页表中维护虚拟页到物理页框的映射,处理器访问内存时由MMU依据页表把虚拟地址转换为物理地址。

image

当物理内存紧张时,操作系统会选择一些页面换出或回收。LRU(最近最少使用)是一种经典的页面置换思想;实际系统通常使用近似LRU等算法,而不是精确实现LRU。

虚拟内存的优点:

  1. 不同进程拥有各自的虚拟地址空间,页表和访问权限使它们通常不能直接访问彼此的内存
  2. 内核地址空间受到权限保护,用户态程序通常不能直接访问内核内存

匿名内存页在需要时可以被换出到磁盘或SSD上的交换空间;文件映射页也可以在回收后从对应文件重新读入。并不是所有内存页都一定能够或需要写入交换空间。

操作系统负责建立和维护页表,并处理缺页异常;硬件MMU(内存管理单元)依据页表完成正常访存时的地址转换和权限检查。

image

物理内存的延时分配(Lazy Allocation)

对于采用按需分页的匿名内存,程序申请内存时通常先获得一段虚拟地址,对应的物理页可以等到程序第一次真正访问该页时再分配。

现代通用操作系统通常会对堆和匿名映射采用这种按需分配方式,但具体行为还取决于分配器、映射方式和操作系统策略。

延迟分配节省内存,但第一次访问尚未映射的页面会触发缺页异常,需要进入内核处理、建立页表映射并重新执行刚才的指令。对于首次写入的匿名页,内核通常还需要分配并提供清零后的物理页。

虚拟地址(Virtual Address)

下图展示的是一种32位Linux虚拟地址空间划分示例,其中用户空间和内核空间的比例取决于具体架构与内核配置,并不是所有系统都固定采用图中的3 GB / 1 GB划分。

image

地址转换后备缓冲器 (TLB )

CPU 生成的地址不是直接对应 RAM 的物理地址,而是当前进程的虚拟地址。

真正访问内存前,需要进行地址转换:虚拟地址 → 查询TLB;若TLB未命中则遍历页表 → 物理地址 → 访问Cache或内存。

处理器的地址转换硬件中设置了很小、很快的TLB,用来缓存近期使用的虚拟页到物理页框的翻译结果及权限信息,避免每次访存都遍历页表。TLB未命中只表示需要查询页表;只有页表中也没有可用映射或访问不符合权限时,才会产生需要操作系统处理的异常。

image

posted @ 2026-10-21 21:07  Miya555  阅读(9)  评论(0)    收藏  举报