2. 多核并行硬件与存储层次

在多核并行科学计算中,算法性能往往并不由浮点运算峰值决定,而是由“数据从哪里来、经过几级缓存、由哪个核访问”决定。理解多核 CPU 的物理结构与存储层次,是判断一个并行算法能否真正跑快的前提。本节从多核架构出发,依次讨论缓存一致性、内存层次、UMA/NUMA 模型,并落到对算法设计的具体影响。

一、多核 CPU 的基本架构

现代多核处理器通常由若干“核 + 私有缓存”组成一个计算单元,多个单元再通过片上互连(如环形总线、网格互连)共享末级缓存(LLC)与内存控制器。每个核内部一般有独立的 L1 指令/数据缓存和 L2 缓存,L3 或 LLC 由若干核共享。核内还包含 SIMD 向量单元,可一次处理多个双精度数,这是科学计算中提升吞吐的重要资源。

从并行算法视角看,这一结构意味着:核与核之间共享数据必须经过缓存层级搬运,而私有缓存中的数据副本可能不一致。因此硬件必须提供缓存一致性机制,软件则必须考虑数据放置与访问局部性。

二、缓存一致性与伪共享

主流多核 CPU 采用 MESI 类协议维护一致性。每个缓存行(常见为 64 字节)处于修改(M)、独占(E)、共享(S)、无效(I)四种状态之一。当一个核写入某行时,其他核中该行的副本被置为无效;若其他核随后读取,则需从拥有最新副本的核或内存重新取数。这一过程保证了共享内存编程模型下“写后读”能看到正确值,但代价是缓存行在核间来回迁移。

对算法设计影响最直接的是伪共享:两个核分别写同一缓存行中的不同变量,虽然逻辑上无冲突,但硬件仍按整行判定冲突,导致该行在两个核之间反复失效与迁移。例如一个并行归约中,若每个线程把自己的部分和写进相邻的数组元素,性能可能比串行还差。解决办法是填充对齐,使每个线程的累加变量独占一个缓存行:

struct alignas(64) PaddedSum {
    double value;
    char pad[64 - sizeof(double)];
};
PaddedSum partial[MAX_THREADS];   // 每个线程写自己的 partial[t].value

这里 alignas(64) 与填充保证每个 value 落在不同缓存行,消除了伪共享。判断是否发生伪共享的常用手段是观察性能计数器中的缓存失效次数,或对比“填充/不填充”两版代码的耗时。

三、内存层次与访问代价

存储层次呈金字塔结构:寄存器、L1、L2、L3、主存、外存,容量逐级增大,延迟逐级升高。典型量级上,L1 命中约几个时钟周期,L2 约十余周期,L3 数十周期,而访问主存可达二百周期以上。科学计算中一次主存访问的代价可能相当于上百次浮点运算,因此“算得动”不等于“喂得饱”。

这直接决定了算法应尽量提高计算访存比,并通过分块(blocking)让工作集落在缓存内。以矩阵乘法 C = A·B 为例,若按行朴素三重循环,B 的访问会频繁跨越缓存行并反复从主存取数;采用分块后,把 A、B、C 各取一个 B×B 子块装入缓存再计算,可显著减少主存流量。分块尺寸应使三个子块之和不超过目标缓存容量,例如 L2 为 256 KB、双精度 8 字节时,粗略取 B 使 3B²×8 ≤ 256K,可得 B 约 100 左右,实际再按缓存行与组相联特性微调。

四、UMA 与 NUMA 模型

对称多处理器(UMA)中,所有核访问内存的延迟一致,共享一条内存通路,扩展性受限。NUMA 架构把内存划分为多个节点,每个节点靠近一组核,核访问本地内存快、访问远端节点内存慢,延迟差可达一倍以上。多路服务器与部分多芯粒处理器都属于 NUMA。

在 NUMA 上,算法的关键不只是“并行”,还有“数据放在哪”。若线程在节点 0 上运行却大量访问节点 1 的内存,就会产生远端访问瓶颈。常用对策是首次触碰(first-touch)策略:内存页在第一次被写时分配到执行该写的核所在节点。因此初始化数组时应由将来主要使用它的线程来写,而不是由主线程统一初始化。也可使用线程亲和性绑定,把线程固定到节点,使计算与数据同侧。

多核存储层次与 NUMA 数据放置 NUMA 节点 0 核 0 L1 L2 私有缓存 核 1 L1 L2 私有缓存 共享 LLC(节点内) 本地内存(节点 0) NUMA 节点 1 核 2 L1 L2 私有缓存 核 3 L1 L2 私有缓存 共享 LLC(节点内) 本地内存(节点 1) 片上互连 / 节点间互连(远端访问延迟更高) 远端访问:核 0 读节点 1 内存,延迟显著高于本地 本地内存 共享 LLC 远端访问路径

五、对算法设计的影响与性能考量

综合以上结构,多核并行算法设计应把握几条原则。第一,数据局部性优先:通过分块、循环重排、数据布局调整(如结构体数组改为数组结构体)提高缓存命中率,减少主存流量。第二,避免伪共享:并行写共享结构时按缓存行隔离,归约、计数器、任务队列等尤其要注意。第三,匹配 NUMA 放置:初始化与计算尽量同节点,必要时绑定线程亲和性,跨节点共享数据要评估远端访问比例。第四,控制同步粒度:一致性维护与锁竞争都会放大缓存行迁移,粗粒度分块加局部归约通常优于细粒度频繁同步。

性能考量上,可用“每字节浮点运算数”粗略判断算法是访存受限还是计算受限;用强扩展与弱扩展实验区分并行开销与数据规模效应;用缓存失效、远端访问占比等计数器定位瓶颈。一个常见教学示例是:在双节点 NUMA 机器上对大规模数组求和,若主线程统一初始化数组,则所有页落在节点 0,节点 1 的线程全部远端访问,扩展效率明显下降;改为各线程按首次触碰初始化自己负责的区间后,远端访问大幅减少,扩展效率随之改善。这类对比说明,硬件结构知识必须转化为具体的数据放置与访问策略,才能兑现并行算法的性能。