内存结构
参考资料:
内存地址到内存条
应用程序使用虚拟地址,CPU 将虚拟地址转换为物理地址,然后将内存读写动作发给内存控制器(在 PC 上就是北桥芯片)。内存控制器将物理地址转换为总线地址(bus address),将具体的总线设备发送读写指令。如果目标物理地址被映射到内存条中,
RAM
现有的 RAM 可以分为两类:SRAM 和 DRAM。SRAM 的全称是 Static Random Access Memory,结构相对复杂,单位体积容量较少,但是存取速度更快,常用作 CPU 内部的寄存器。DRAM 全称 Dynamic Random Access Memory,结构更简单,单位体积容量更大,但是存取速度要比 SRAM 慢,常用作计算机运行内存。
SRAM 和 DRAM 都属于易失性存储介质(volatile memory),也就是断电之后存储在其中的数据都会丢失。DRAM 需要不断地进行再充电(refresh),否则存储的电荷会逐渐消失,因此,“刷新频率”也是现有的内存产品的关键指标之一。
目前常见的商品内存条是 DDR3 或 DDR4,都属于 DRAM 类型。
DRAM 子系统
DRAM 内部还有更细化的结构,依据层级大小可以划分为 channel > DIMM > rank > chip > bank > row/column
。
channel(通道)可能只有一个,DIMM 通常就是一个内存条包装。rank(路)
最基本的数据单元(storage cell)的结构是二维矩阵,叫做 Chip,每一行就叫做一个 row,每一列叫做一个 column。此外还有一个 row-buffer,是执行内存写入和读取操作时的缓冲区。
对一个 row/column 矩阵执行的读写操作单位是 bit,也就是对于一个 row/column 矩阵,只能一次读一个 bit,写一个 bit。
但是 CPU 往往一次操作多个 bit,例如对于 64-bit 的 CPU,每次内存读写操作对应 64 个 bit。因此,同时有 64 个 CHIP 同时工作,如果是启用了 ECC 的内存,则有 72 个。
访问周期
DRAM 也有自己的频率,但是一次内存访问并不能在一个周期之内完成。虽然 DDR 内存可以在一个周期之内传递两个机器字的数据,但是寻址需要花费很多时间。
之间说过,最基本的 chip 组织成二维矩阵的形式,因此在访问内存的时候,需要指定要访问其中哪一行、哪一列。指定行的过程叫做 RAS(Row Address Selection),指定列的过程叫做 CAS(Column Address Selection)。RAS 和 CAS 是串行进行的,RAS 之后,内存会把选定的行中的数据放入 row-buffer 里。如果相邻两次内存访问的 RAS 取值相同,那么可以省去第二次的 RAS,提升速度。
访问一次内存消耗的时间较长,因此内存通过增加数据吞吐量的方法来补偿。
memory allocator 优化
有些 memory allocator 已经开始考虑缓存的影响了,让数据尽量位于同一个缓存行(cache line)之中。类似,考虑了 DRAM 结构之后,可以让数据尽可能位于同一个 row-buffer 之中,这样,访问相同 row 可以提升一定的速度。
此外,让不同的线程使用不同的 bank,这样不同 bank 可以独立进行寻址,访问内存在硬件层次上可以并行。
尽可能让不同 thread 使用的物理页对应相同的 cache line,对应不同的 bank,因此相关逻辑应该在 page frame allocator 中实现,配合 page coloring 技术。
dmidecode
dmidecode 是 Linux 系统下的一个命令行工具,能够读取 DMI 表(也有的说法是 SMBIOS 表),通过这个工具,可以获取内存的相关硬件参数。命令语法是
dmidecode -t <type>
其中 type 字段的合法取值与解释如下:
type | information |
---|---|
0 | BIOS |
1 | System |
2 | Base Board |
3 | Chassis |
4 | Processor |
5 | Memory Controller |
6 | Memory Module |
7 | Cache |
8 | Port Connector |
9 | System Slots |
10 | On Board Devices |
11 | OEM Strings |
12 | System Configuration Options |
13 | BIOS Language |
14 | Group Associations |
15 | System Event Log |
16 | Physical Memory Array |
17 | Memory Device |
18 | 32-bit Memory Error |
19 | Memory Array Mapped Address |
20 | Memory Device Mapped Address |
21 | Built-in Pointing Device |
22 | Portable Battery |
23 | System Reset |
24 | Hardware Security |
25 | System Power Controls |
26 | Voltage Probe |
27 | Cooling Device |
28 | Temperature Probe |
29 | Electrical Current Probe |
30 | Out-of-band Remote Access |
31 | Boot Integrity Services |
32 | System Boot |
33 | 64-bit Memory Error |
34 | Management Device |
35 | Management Device Component |
36 | Management Device Threshold Data |
37 | Memory Channel |
38 | IPMI Device |
39 | Power Supply |
可见,与内存相关的取值有 5、6、16、17、18、19、20、33、37。
案例——PALLOC
论文《PALLOC: DRAM Bank-Aware Memory Allocator for Performance Isolation on Multicore Platforms》。
现有的 OS 都把 DRAM 看作完整的一坨,不考虑内部的细化结构,也就是多少 rank、多少 bank 等一概不考虑。当然,这样开发起来更加容易,而且硬件设计者也在尽力弥合细节的性能影响。但不可否认的是,内部结构确实对性能有影响,既然这样,我们就有理由去追求这些性能提升。
由于内存由多个 bank 组成,在多核环境下,访问不同 bank 会造成性能的差异。如果一个多线程的应用程序,不同的线程运行在不同的 CPU 上,分配内存时,不同的线程使用不同 bank 中的内存,这样可以显著地提升性能。
但是 PALLOC 的目的并不是提升性能,而是增强隔离性和可预测性,适合于嵌入式系统。也就是说,避免因使用相同/不同 bank 而造成的多线程应用性能波动。PALLOC 使 thread 之间的差异变小,但是可能造成单个 thread 的性能小幅下降(因为可用的内存变小了)。
PALLOC 用的是类似内存分区的方式,划定某个 bank 归哪个 CPU 使用之后,这个 bank 就不可能被其他 CPU 使用。
重点: - 在一个 CPU 上申请的内存不一定就在这个 CPU 上使用。但这并不是 PALLOC 的问题,而是程序设计这个问题。一个“好习惯”就是自己分配自己使用的内存。 - CPU 数量和 bank 数量未必相等,有可能 bank 数量比 CPU 数量少(类似 PCID 的分配)。对于 PALLOC,由于是面向嵌入式系统的,需要设计人员人工指定每个 bank 供哪个 Core 使用(可以在运行时动态更改配置)。 - 文章仅仅考虑了 bank,但是实际上 DRAM 的结构不仅仅有 bank,如果考虑更细的结构(例如 row)是不是性能提升更好? - bank 的粒度是多大?应该是页大小的整数倍,而且一个 bank 应该映射到连续的物理地址范围。既然需要关心物理地址,那么必然需要编写内核代码。
案例——High Efficiency General Memory Allocator
论文《High Efficiency General Memory Allocator》。
主要特点是把软件底层和上层特征统一了起来。内存分配器会根据软件的上层行为调整内存分配策略。
许多软件都实现了自定义的内存分配器,例如在 SSL、LUA 等项目中,都实现了某种类似内存池的机制。但是每个软件都定制一个 allocator 比较麻烦,这篇文章的目的就是开发一个通用的 allocator,能实现更高的性能,也可以实现通用性。
两个假设: - 如果程序中大量进行内存分配,那么这些内存分配基本上都是在循环中进行的。 - 循环中进行的内存分配通常都是大小相同的。
文章借助编译器对程序进行 instrumentation,利用 LLVM 识别循环和 malloc/free 的调用信息。
NUMA
NUMA 表示非均匀内存访问,也就是在系统中,每个处理器访问内存的速度是不同。对于 x86 处理器来说,如果一个计算机中只安装了一块多核处理器,那么这个计算机是一个标准的 UMA 架构,因为同一个硅片上的逻辑处理共享同一个内存控制器(原北桥芯片),因此它们看到的内存结构是相同的。
如果计算机主板上有多个CPU插槽,情况就不一样了。由于每个CPU包装(package)都集成了自己的内存控制器,因此不同插槽上的处理器所看到的内存并不是均匀的。每个插槽(或者节点)都有一部分与其关联的内存,访问这部分内存速度较快;访问其他节点关联的内存则速度较慢。因此,对于单节点的多核处理器,并不是NUMA结构,NUMA的特点仅仅在多处理器的情况下才会体现出来。
所以,虽然x86使用了NUMA架构,但是对于普通桌面系统而言,并不需要去考虑。只有那些高性能的服务器才需要考虑NUMA的影响。