6计算机体系结构:构造式方法

并行处理

多核与 SIMD

多核SIMD线程GPU

🎯 AI 时代的学习建议:不要只读理论。理解这一章的设计动机(为什么需要这个概念?)和边界条件(什么情况下会失效?)。当 AI 生成的代码涉及这些概念时,你能判断它是否用对了场景。AI 能帮你总结内容,但无法替代你亲手阅读、思考、建立直觉的过程。

第6章 并行处理(多核与 SIMD) Parallel Processing: Multi-Core and SIMD

一、导读

1.1 本章学习目标

在本章结束时,你将能够:

  • 理解并行计算的基本原理,掌握 Amdahl 定律和 Gustafson 定律对并行加速比的限制
  • 分析多核处理器的架构设计,包括共享内存、分布式内存和混合架构的特点与适用场景
  • 掌握 SIMD 指令集的工作原理,理解 x86 的 SSE/AVX/AVX-512 和 ARM NEON 等向量扩展如何利用数据级并行
  • 理解 GPU 架构的核心设计,包括 SIMT 执行模型、线程层次结构和内存层次
  • 掌握线程同步的基本机制,包括互斥锁、信号量、原子操作和硬件事务内存
  • 分析并行编程模型,包括共享内存模型(OpenMP、pthreads)和消息传递模型(MPI)
  • 识别和解决并行编程中的常见问题,如竞争条件、死锁、活锁和 starvation

1.2 为什么本章如此重要

并行计算已经从专业领域走向了主流计算。2000 年代中期,处理器频率提升遇到功耗墙(power wall),行业从单核频率竞赛转向多核并行架构。如今,从智能手机的双核/四核处理器到数据中心的 64 核/128 核服务器,从嵌入式 DSP 的 SIMD 指令到数据中心 GPU 的数千个计算核心,并行处理无处不在。

这一转变对软件开发者提出了新的要求。在单核时代,程序性能的提升可以依赖硬件频率的增长("免费的午餐")。但在多核时代,如果程序不能利用并行性,它的性能将被锁定在单个核心的能力范围内。掌握并行编程不再是可选技能,而是现代软件开发的基本要求。

并行计算的挑战不仅在于技术层面,更在于思维方式的转变。串行程序的行为是确定性的——给定相同的输入,总是产生相同的输出。并行程序的行为可能因为线程调度顺序的不同而产生不同的结果,这种不确定性使得并行程序的调试和验证远比串行程序困难。

从体系结构角度看,并行处理引入了大量新的设计问题。缓存一致性(上一章讨论的 MESI 协议)、内存一致性模型(顺序一致性、总序一致性、松弛一致性等)、互连网络拓扑、同步机制的硬件支持等,都是并行系统设计的核心挑战。理解这些底层机制有助于编写更高效的并行程序。

1.3 核心问题引导

在阅读本章时,请思考以下问题:

为什么不能简单地通过提高时钟频率来持续提升性能?功耗墙的物理根源是什么?

Amdahl 定律对并行加速比施加了什么限制?如果程序的串行部分占 10%,使用 1000 个核心最多能加速多少倍?

共享内存和消息传递两种并行模型各有什么优缺点?为什么两种模型在现实中并存?

SIMD 和多线程并行有什么本质区别?它们分别适合什么样的计算模式?

GPU 的 SIMT 模型与 CPU 的 SIMD 模型有什么不同?为什么 GPU 需要 warp 调度器?

为什么竞争条件如此难以调试?有哪些硬件和软件机制可以帮助避免竞争条件?

什么是内存一致性模型?为什么不同的处理器架构采用不同的内存一致性模型?


二、核心概念详解

2.1 并行计算的理论基础

并行计算的理论基础由几个核心定律和概念构成。

Amdahl 定律描述了在固定工作量下,并行化对加速比的限制。假设程序中串行部分的比例为 s,并行部分可以被完美加速到 p 个处理器,则总加速比为:

Speedup(s, p) = 1 / (s + (1 - s) / p)

当 p 趋向无穷大时,最大加速比趋近于 1/s。这意味着即使有无限多的处理器,加速比也受限于串行部分的比例。如果 10% 的代码是串行的,最大加速比只有 10 倍,无论使用多少处理器。

Gustafson 定律提供了另一个视角:随着处理器数量增加,问题规模也可以相应增大。如果串行部分的工作量固定而并行部分的工作量随处理器数量线性增长,则加速比为:

Speedup(p) = p - (p - 1) * s

Gustafson 定律表明,在实际应用中(问题规模可变),并行化可以获得接近线性的加速比。这解释了为什么超算中心可以使用数千个处理器获得显著的性能提升。

并行计算的分类(Flynn 分类法):

  • SISD(单指令单数据流):传统单核处理器,每条指令操作一个数据
  • SIMD(单指令多数据流):一条指令同时操作多个数据元素,如向量处理器
  • MISD(多指令单数据流):理论上存在但实际很少使用
  • MIMD(多指令多数据流):多个处理器独立执行不同的指令,如多核处理器

现代计算机系统通常混合使用 SIMD 和 MIMD 并行。例如,一个 8 核处理器,每个核心支持 AVX-512(512 位 SIMD,可同时处理 16 个 32 位整数),则总的并行度为 8 * 16 = 128。

2.2 多核处理器架构

多核处理器在单个芯片上集成多个处理核心,是现代高性能计算的主力。多核架构的核心设计决策包括:

核心组织方式:同构多核(所有核心相同,如 ARM big.LITTLE 之前的设计)和异构多核(不同类型核心共存,如 ARM big.LITTLE 将高性能核心和高能效核心组合在一起)。异构设计可以根据工作负载动态分配任务到合适的核心,在性能和能效之间取得更好的平衡。

互连拓扑:核心之间如何通信是多核设计的关键问题。总线互连(所有核心共享一条总线)简单但扩展性差;环形互连(core ring)适用于中等规模(8-16 核);网格互连(mesh)适用于大规模(数十到数百核);更高级的拓扑如 torus、fat tree 用于超算互连。

缓存组织:每核私有缓存(L1/L2)加共享缓存(L3)是最常见的配置。共享缓存可以减少核心间的通信延迟(通过缓存一致性协议在 L3 级别共享数据),但也引入了缓存一致性的复杂性。

内存控制器集成:现代处理器将内存控制器集成在芯片上(如 AMD 的 Infinity Fabric、Intel 的 mesh 架构),减少了内存访问延迟。NUMA(非统一内存访问)架构中,每个核心访问本地内存的速度快于访问远程内存,这对操作系统和应用程序的内存分配策略有重要影响。

多核设计避免了单核频率提升带来的功耗问题(功耗与频率成正比,与电压的平方成正比),通过增加核心数量来提高吞吐量。然而,这也要求软件能够有效地利用多个核心,否则增加的核心只会浪费芯片面积和功耗。

2.3 SIMD 向量处理

SIMD(Single Instruction, Multiple Data)是一种数据级并行技术,一条指令同时对多个数据元素执行相同的操作。SIMD 特别适合规则的数据并行计算,如向量运算、图像处理、信号处理等。

x86 架构的 SIMD 扩展经历了多代演进:

  • MMX(1997):64 位 SIMD,8 个 8 位整数或 4 个 16 位整数或 2 个 32 位整数
  • SSE(1999):128 位 SIMD,新增 16 个 128 位 XMM 寄存器,支持浮点 SIMD
  • AVX(2011):256 位 SIMD,XMM 寄存器扩展为 YMM(256 位),支持 8 个单精度浮点或 4 个双精度浮点
  • AVX2(2013):扩展整数 SIMD 到 256 位,引入 gather 指令
  • AVX-512(2016):512 位 SIMD,新增 ZMM 寄存器,支持 16 个单精度浮点或 8 个双精度浮点
c
// 标量版本:每次处理 1 个元素
for (int i = 0; i < N; i++)
  C[i] = A[i] + B[i];

// AVX 版本:每次处理 8 个 float 元素
for (int i = 0; i < N; i += 8) {
  __m256 a = _mm256_loadu_ps(&A[i]);
  __m256 b = _mm256_loadu_ps(&B[i]);
  __m256 c = _mm256_add_ps(a, b);
  _mm256_storeu_ps(&C[i], c);
}

SIMD 编程的挑战包括:数据对齐(未对齐的数据访问可能导致性能下降或异常)、分支处理(SIMD 不支持传统分支,需要使用掩码操作)、循环向量化(编译器自动向量化能力有限,复杂循环可能需要手动编写 SIMD 代码)。

2.4 GPU 架构与通用计算

GPU(Graphics Processing Unit)最初为图形渲染设计,但其大规模并行架构使其成为通用计算(GPGPU)的强大平台。GPU 的核心设计理念是用大量简单的处理单元来实现高吞吐量,而非用少量复杂的处理单元来实现低延迟。

GPU 的架构特点包括:

SIMT 执行模型(Single Instruction, Multiple Threads):GPU 将线程组织为 warp(NVIDIA,32 个线程)或 wavefront(AMD,64 个线程)。warp 中的所有线程在同一时刻执行同一条指令,但操作不同的数据。这与 CPU 的 SIMD 类似,但更灵活——每个线程有自己的寄存器和执行路径。

线程层次结构:GPU 线程被组织为线程块(thread block),线程块内的线程可以通过共享内存(shared memory)通信和同步。多个线程块组成一个内核(kernel),不同线程块之间不能直接通信。

内存层次:GPU 拥有多层次的内存结构:全局内存(高延迟、大容量、所有线程可见)、共享内存(低延迟、小容量、线程块内可见)、寄存器(最低延迟、每个线程私有)、L1/L2 缓存(自动缓存全局内存访问)。

分支分歧(Branch Divergence):当同一 warp 中的线程执行不同的分支路径时,GPU 必须串行执行所有路径(先执行 if 分支的线程,禁用其他线程;再执行 else 分支的线程,禁用其他线程)。这会导致性能下降,是 GPU 编程中需要特别注意的问题。

CUDA(Compute Unified Device Architecture)是 NVIDIA 的 GPU 编程平台,提供了 C 语言扩展来编写 GPU 内核。OpenCL 是跨厂商的开放标准,支持多种并行设备。

2.5 线程同步与并发控制

并行编程的核心挑战之一是管理多个线程对共享资源的访问。如果多个线程同时修改同一数据而不加协调,会导致竞争条件(race condition),产生不可预测的结果。

互斥锁(Mutex):最基本的同步机制。线程在访问共享资源前获取锁,访问完成后释放锁。如果锁已被其他线程持有,请求线程将被阻塞。互斥锁的问题是可能导致死锁(两个线程互相等待对方释放锁)和优先级反转(低优先级线程持有锁,阻塞高优先级线程)。

信号量(Semaphore):互斥锁的泛化,维护一个计数器,允许最多 N 个线程同时访问资源。N=1 时等价于互斥锁。

原子操作(Atomic Operation):不可被中断的操作,由硬件保证原子性。常见的原子操作包括原子加(atomic add)、原子比较并交换(CAS, Compare-And-Swap)等。原子操作比锁更轻量,但只能实现简单的同步模式。

c
// 使用原子操作实现线程安全的计数器
atomic_int counter = 0;

void increment() {
  atomic_fetch_add(&counter, 1);
}

硬件事务内存(HTM, Hardware Transactional Memory):将事务的概念引入硬件。线程可以开始一个事务,执行一系列内存操作,然后尝试提交。如果事务执行期间没有其他线程修改了相关数据,则提交成功;否则事务回滚,线程需要重试。HTM 简化了并发编程,但硬件实现复杂,事务大小受限。

无锁数据结构(Lock-Free Data Structure):使用原子操作实现的并发数据结构,不依赖锁。无锁结构避免了死锁问题,但设计复杂,正确性证明困难。经典的无锁结构包括 Michael-Scott 队列、无锁哈希表等。


三、深入分析与讨论

3.1 内存一致性模型

内存一致性模型(Memory Consistency Model)定义了多处理器系统中内存操作的可见顺序。在单核系统中,内存操作按照程序顺序执行,结果可预测。但在多核系统中,编译器和处理器的优化(如指令重排、缓存、写缓冲)可能导致其他核心观察到的操作顺序与程序顺序不同。

顺序一致性(Sequential Consistency):最直观的模型,所有处理器的操作按某种全局顺序交错执行,每个处理器的操作按程序顺序出现。这是最严格的模型,但实现代价高(禁止任何重排)。

总序一致性(Total Store Ordering, TSO):x86 处理器采用的模型。允许写-读重排(后续读可以越过前面的写),但保持其他所有顺序。TSO 相对严格,使得 x86 上的并发程序更容易推理。

松弛一致性(Relaxed Consistency):ARM 和 RISC-V 采用的模型,允许更多的重排以获取更高性能。程序员需要使用显式的内存屏障(fence/barrier)指令来保证必要的顺序。这种模型对硬件更友好,但对程序员更困难。

内存一致性模型的选择反映了性能与易用性之间的权衡。x86 的严格模型使编程更容易但限制了硬件优化空间;ARM/RISC-V 的松弛模型给硬件更多自由度但要求程序员显式处理内存顺序。

3.2 并行编程模型与实践

并行编程模型为开发者提供了组织并行计算的高层抽象。

共享内存模型:所有线程共享同一地址空间,通过读写共享变量进行通信。优点是编程相对简单(不需要显式消息传递);缺点是需要同步机制来管理并发访问,且扩展性受限于共享总线或互连的带宽。典型实现包括 POSIX threads(pthreads)、OpenMP(编译器指令驱动的并行化)和 Cilk(任务并行模型)。

c
// OpenMP 示例:并行 for 循环
#pragma omp parallel for
for (int i = 0; i < N; i++) {
  C[i] = A[i] + B[i];
}

消息传递模型:每个进程拥有独立的地址空间,通过显式的消息传递进行通信。优点是可扩展性好(适合分布式系统);缺点是编程复杂度高(需要手动管理通信和数据分布)。典型实现是 MPI(Message Passing Interface)。

数据并行模型:开发者描述对数据集的操作,运行时系统负责将数据分配到多个处理器。GPU 编程(CUDA、OpenCL)和数组编程语言(如 NumPy)都属于这一模型。

3.3 功耗与暗硅问题

并行架构的兴起与功耗约束密切相关。动态功耗公式 P = alpha C V² * f 表明,功耗与频率成正比、与电压的平方成正比。为了将频率从 3GHz 提升到 6GHz,电压需要大幅增加,导致功耗呈指数增长,散热成为不可逾越的障碍。

多核设计通过降低每个核心的频率(从而降低电压)来在相同的功耗预算内提供更多的计算能力。然而,这也带来了"暗硅"(Dark Silicon)问题:由于功耗限制,芯片上不能同时激活所有晶体管。在 28nm 及以下工艺中,可能只有 20-50% 的芯片面积可以在任何时候处于活跃状态。

暗硅问题推动了异构计算的发展:将不同类型的计算单元(CPU 核心、GPU、DSP、专用加速器)集成在同一芯片上,根据工作负载动态激活合适的单元。ARM 的 big.LITTLE 架构就是这一思路的典型代表。


四、实践案例

4.1 案例:使用 SIMD 优化矩阵向量乘法

矩阵向量乘法 y = A * x 是科学计算中最基本的操作之一。使用 AVX2 指令集可以显著加速这一操作:

c
// 标量版本
void matvec_scalar(float *A, float *x, float *y, int N) {
  for (int i = 0; i < N; i++) {
    float sum = 0;
    for (int j = 0; j < N; j++)
      sum += A[i * N + j] * x[j];
    y[i] = sum;
  }
}

// AVX2 向量化版本
void matvec_avx(float *A, float *x, float *y, int N) {
  for (int i = 0; i < N; i++) {
    __m256 sum = _mm256_setzero_ps();
    for (int j = 0; j < N; j += 8) {
      __m256 a = _mm256_loadu_ps(&A[i * N + j]);
      __m256 xv = _mm256_loadu_ps(&x[j]);
      sum = _mm256_fmadd_ps(a, xv, sum);
    }
    // 水平归约
    float result[8];
    _mm256_storeu_ps(result, sum);
    y[i] = result[0] + result[1] + result[2] + result[3]
         + result[4] + result[5] + result[6] + result[7];
  }
}

AVX2 版本每次处理 8 个浮点数,理论上可以获得接近 8 倍的加速。实际加速取决于内存带宽、数据对齐和循环展开程度。

4.2 案例:CUDA 实现并行归约

并行归约(parallel reduction)是将一个数组的所有元素合并为单个值的操作(如求和、求最大值)。CUDA 中的并行归约展示了 GPU 线程协作的基本模式:

cuda
__global__ void reduce_sum(float *input, float *output, int N) {
  __shared__ float sdata[256];
  int tid = threadIdx.x;
  int i = blockIdx.x * blockDim.x + threadIdx.x;

  sdata[tid] = (i < N) ? input[i] : 0;
  __syncthreads();

  for (int s = blockDim.x / 2; s > 0; s >>= 1) {
    if (tid < s)
      sdata[tid] += sdata[tid + s];
    __syncthreads();
  }

  if (tid == 0)
    output[blockIdx.x] = sdata[0];
}

这个实现使用共享内存在线程块内进行归约,每个线程块产生一个部分和。最终结果需要再次归约(可以用另一个内核或 CPU 完成)。优化技术包括消除 warp 分歧、使用 warp 级原语(__shfl_down_sync)等。


五、常见误区与注意事项

误区一:认为增加核心数量就能线性提升性能。 Amdahl 定律表明,程序的串行部分限制了最大加速比。此外,并行化本身有开销(线程创建、同步、通信),在核心数量较少时这些开销可能抵消并行化的收益。实际加速比通常远低于核心数量。

误区二:认为 GPU 适合所有计算任务。 GPU 的优势在于大规模数据并行计算,需要足够的并行度来填满数千个处理单元。对于串行性强、分支多、数据依赖复杂的应用,GPU 的性能可能不如 CPU。此外,CPU 和 GPU 之间的数据传输(PCIe 总线)也有显著延迟,如果计算量不足以抵消传输开销,使用 GPU 反而更慢。

误区三:认为使用锁就能保证并发正确性。 锁的使用需要谨慎设计。粒度过粗(一把大锁保护所有共享数据)会导致严重的串行化;粒度过细(很多细粒度的锁)增加死锁风险和管理复杂度。此外,锁不能解决所有并发问题——某些数据结构需要无锁算法才能保证正确性和性能。

误区四:忽视内存一致性模型的影响。 在 x86 上正确运行的并发程序可能在 ARM 上出现错误,因为两者的内存一致性模型不同。编写可移植的并发代码需要使用标准的原子操作和内存序(memory order)语义,而不是依赖特定架构的行为。C++11 和 Java 的内存模型提供了跨平台的并发语义定义。

误区五:认为并行编程只是"加分项"。 随着多核处理器的普及和单核性能增长的放缓,并行编程已经从"可选技能"变为"必备技能"。不能利用多核的程序将在性能竞争中逐渐落后。尽早学习并行编程的思维和技能,对于任何软件开发者都至关重要。


六、本章小结

本章全面讲解了并行处理的各种架构和技术。随着频率提升遇到功耗墙,多核并行成为性能增长的主要途径。多核处理器通过增加核心数量提高吞吐量,SIMD 指令集利用数据级并行加速向量运算,GPU 提供大规模数据并行能力。并行计算的理论基础(Amdahl 定律、Gustafson 定律)揭示了并行加速的极限。线程同步机制(锁、原子操作、事务内存)是保证并发正确性的关键。内存一致性模型定义了多处理器系统中内存操作的可见顺序,不同架构采用不同的模型以平衡性能与易用性。理解这些并行架构和编程模型,有助于选择合适的并行策略,充分发挥现代硬件的性能。

七、思考题与练习

如果一个程序 20% 的执行时间是串行的,使用 4 核、16 核、64 核处理器分别能获得多少加速比?使用 Amdahl 定律计算。

比较 SIMD 和多线程并行的适用场景。什么样的计算适合 SIMD?什么样的计算适合多线程?

用 AVX2 指令集实现一个向量点积的优化版本,并与标量版本进行性能比较。

解释 GPU 中"分支分歧"的问题。给出一个导致严重分支分歧的代码示例,并说明如何优化。

编写一个使用 pthreads 的多线程程序,演示竞争条件的发生。然后使用互斥锁修复竞争条件。

比较 x86(TSO)和 ARM(松弛一致性)的内存一致性模型。写出一段在 x86 上正确但在 ARM 上可能出错的并发代码。

分析暗硅问题对处理器设计的影响。异构计算如何解决暗硅问题?举出具体的商业实现案例。

研究 RISC-V 的向量扩展(V 扩展)的设计特点,比较它与 x86 AVX 的异同。RISC-V V 扩展的可变长度向量设计有什么优势?

6/6并行处理
0%