现代处理器:DLP 与 TLP
如果说 ILP 是在时间维度上重叠指令执行,那么 DLP(Data-Level Parallelism)就是在空间维度上同时处理多份数据。SIMD(Single Instruction, Multiple Data)是 DLP 的核心实现方式。
想象你有一堆相同的加法要做:给数组中每个元素加 1。标量处理器需要逐个执行:
for i in range(n):
a[i] = a[i] + 1 # 一次只算一个
SIMD 处理器可以一次性对 4 个、8 个甚至 16 个元素同时做加法:
// AVX-512: 512bit 寄存器 = 16 x 32bit float
__m512 vec = _mm512_load_ps(a);
vec = _mm512_add_ps(vec, one_vec); // 一次加 16 个
_mm512_store_ps(a, vec);
标量代码需要 n 次循环迭代、n 次分支判断、n 次地址计算。SIMD 代码把循环展开和指令融合打包在硬件层面完成,控制开销被摊薄到接近零。
主流 SIMD 指令集演进:
| 指令集 | 位宽 | 同时操作数 | 代表平台 |
|---|---|---|---|
| SSE | 128 bit | 4 x float | Intel x86 (1999) |
| AVX | 256 bit | 8 x float | Intel Sandy Bridge (2011) |
| AVX-512 | 512 bit | 16 x float | Intel Skylake-X (2017) |
| NEON | 128 bit | 4 x float | ARM (2005) |
| SVE/SVE2 | 可变 128-2048 bit | 灵活 | ARMv8-A+ (2016) |
一个经典案例是课件中提到的 sin(x) 泰勒展开:$\sin(x) = x - \frac{x^3}{3!} + \frac{x^5}{5!} - \frac{x^7}{7!} + \cdots$。用 SIMD 向量化后,可以同时计算多个 x 值的 sin 值,理论加速比等于 SIMD 宽度(4x-16x)。实际中由于内存带宽和加载/存储开销,通常能达到 3x-8x。
当 ILP 和 DLP 都挖完之后,处理器设计师把目光转向了更高层次的并行——线程级并行(TLP)。既然一个核心跑不快,那就多放几个核心。
多核处理器的演进轨迹:
- 2005:Intel 双核 Pentium D,"胶水"多核
- 2006:Intel Core 2 Duo,原生双核
- 2008:Intel Core i7,四核 + 超线程
- 2017:AMD Ryzen Threadripper,16 核
- 2023:AMD EPYC Genoa,96 核
- 2024:Apple M3 Ultra,32 核
多核处理器面临的核心挑战是缓存一致性(Cache Coherence)。当多个核心同时读写同一块内存时,它们的私有缓存(L1/L2)中的数据副本可能不一致。需要硬件协议来保证「任何时刻,所有核心看到同一地址的数据是一致的」。
MESI 协议是最经典的缓存一致性协议,定义了每个缓存行的四种状态:
- Modified:已修改,与主存不一致,独占总线
- Exclusive:独占,与主存一致,只有一个缓存拥有
- Shared:共享,与主存一致,多个缓存同时拥有
- Invalid:无效,数据已过时
当核心 A 写入一个 Shared 状态的缓存行时,需要向总线广播 Invalidate 消息,让其他核心作废自己的副本。这个广播操作就是缓存一致性流量,是多核扩展的主要瓶颈之一。
多核需要复制整个核心(ALU、寄存器堆、缓存等),面积开销大。超线程(Simultaneous Multi-Threading, SMT)是一种更经济的方案:只复制指令调度和寄存器状态,共享执行单元。
想象一个核心有两套「前台」(程序计数器、寄存器、重命名表),但只有一个「后台」(ALU、Load/Store 单元)。当线程 A 在等待缓存未命中时,线程 B 可以占用空闲的执行单元继续干活。
Intel 超线程通常带来 15-30% 的性能提升,代价只有约 5% 的核心面积增加。但如果两个线程同时竞争同一类资源(如浮点单元),性能反而可能下降。
现代处理器的 SMT 宽度:Intel x86 通常 2-way SMT(一个核心 2 线程),IBM POWER 可达 8-way SMT。
一个需要注意的陷阱是伪共享(False Sharing):两个线程修改不同变量,但这两个变量恰好落在同一个缓存行(通常 64 字节)。虽然逻辑上没有数据竞争,但缓存一致性协议会在两个核心之间不断来回无效化该缓存行,导致性能暴跌。
三种并行层次不是互斥的,而是互补的。一个优化良好的程序通常会同时利用所有层次:
| 并行类型 | 粒度 | 谁负责发现 | 适用场景 | 典型加速比 |
|---|---|---|---|---|
| ILP | 指令级 | 硬件动态调度 | 通用计算 | 2-4x |
| DLP (SIMD) | 数据级 | 编译器/程序员 | 向量运算、图像处理 | 4-16x |
| TLP (多核) | 线程级 | 程序员/运行时 | 独立任务、服务请求 | 核心数 x |
实际编程中的选择策略:
- 循环体内有大量相同操作 → 先尝试 SIMD 向量化
- 循环之间存在数据依赖 → SIMD 受限,考虑多线程分块
- 任务之间完全独立 → 多线程/多进程并行
- 既有数据并行又有任务并行 → 混合:多线程外层 + SIMD 内层
- ARM SVE 的可变向量长度设计有什么优势?为什么比固定宽度的 AVX-512 更适合未来处理器?
- 在 64 核心服务器上运行单线程程序,为什么性能可能不如在 8 核心上?(提示:NUMA、缓存拓扑)
- 编写一个会触发 False Sharing 的 C 程序,并用
perf c2c检测缓存行竞争。如何修复? - Apple M 芯片的「性能核 + 能效核」设计,在 TLP 调度上有什么独特挑战?操作系统如何决定线程放在哪种核心上?