CppCon 2022 学习: GPU Accelerated Computing and Optimizations on Cross-Vendor Graphics Cards with Vu
并行计算简介
1. 并行计算的发展历程
- 1970s:超算(Supercomputers)用于科学计算
- CMU C.mmp(1971):16 个 PDP-11 处理器
- Cray XMP(1984):4 个向量处理器
- Thinking Machines CM-2(1987):65536 个 1-bit 处理器 + 2048 个浮点协处理器
- Blacklight 超算(今天):4096 核心
- 1990s:数据库应用推动并行计算
- Sun Enterprise 10000(1997):16 个 UltraSPARC-II
- Oracle Supercluster M6-32(今天):32 个 SPARC M2
- 2004 年:Intel 达到功率密度墙(Power Density Wall)
- 单核性能提升受限
- 并行处理成为性能提升的必然选择
2. 单处理器性能历史
- 更宽的数据通路(4-bit → 64-bit)
- 更高效的流水线(CPI 从 3.5 → 1.1)
- 指令级并行(ILP,Superscalar,每周期最多发射 4 条指令)
- 更快的时钟频率(10 MHz → 3 GHz)
- 80-90 年代性能指数增长,但 2004 后趋缓
3. 并行计算机类型
- 桌面/移动设备:
- Mac Pro:12 核 Xeon E5
- MacBook Pro:4 核 i7
- iPhone 6s:2 核 CPU + GPU
- 超级计算机:
- Titan 超算:18688 个 16 核 CPU + NVIDIA K20X GPU
- 专用加速器:
- Intel Xeon Phi:61 个 x86 核(1.3 GHz)
- NVIDIA GTX 980:多块 GPU 处理单元
并行编程概念
1. 什么是并行计算机?
- 定义:多个处理单元合作快速解决问题
- 关注 性能 和 效率
- 与“并发编程”不同,后者更多是程序逻辑的正确性
2. 并行计算动机
- 加速(Speedup):
Speedup=单处理器时间多处理器时间 \text{Speedup} = \frac{\text{单处理器时间}}{\text{多处理器时间}} Speedup=多处理器时间单处理器时间 - 影响加速的因素:
- 通信成本:通信越多,速度提升受限
- 工作分配不均:部分处理器空闲影响效率
3. 并行思维(Parallel Thinking)
- 将任务分解成可并行执行的部分
- 分配任务到处理器
- 管理通信和同步,避免限制加速
4. 硬件视角
- 了解硬件特性很重要(通信速度、功耗、性能/成本权衡)
- 效率 ≠ 速度:程序快不代表硬件利用率高
- CPU 设计变化:
- 2004 前:单核性能为主
- 2004 后:性能/面积/功耗为主,核心数增加,注重每瓦性能
例子概述
目标:计算数组中每个浮点数 x[i] 的 sin 值。
方法:用 Taylor 展开式近似:
sin(x)=x−x33!+x55!−x77!+…
\sin(x) = x - \frac{x^3}{3!} + \frac{x^5}{5!} - \frac{x^7}{7!} + \dots
sin(x)=x−3!x3+5!x5−7!x7+…
C++ 程序逻辑
void sinx(int N, int terms, float* x, float* result) {
for (int i = 0; i < N; i++) { // 遍历数组每个元素
float value = x[i]; // 初始值 x
float numer = x[i] * x[i] * x[i]; // 分子初始值 x^3
int denom = 6; // 分母初始值 3! = 6
int sign = -1; // 符号轮换
for (int j = 1; j <= terms; j++) { // Taylor 展开迭代 terms 次
value += sign * numer / denom; // 加上当前项
numer *= x[i] * x[i]; // 更新分子 x^(2j+3)
denom *= (2*j+2)*(2*j+3); // 更新分母 (2j+2)*(2j+3)
sign *= -1; // 更新符号
}
result[i] = value; // 存储结果
}
}
要点:
- 外层循环
for (i)→ 遍历数组每个元素 - 内层循环
for (j)→ 计算 Taylor 展开的每一项 - 每次迭代更新:
- 分子:
numer *= x[i]^2 - 分母:阶乘累乘
- 符号:正负交替
- 分子:
汇编对应(伪示意)
x[i] ; 加载 x[i] 到寄存器
ld r0, addr[r1] ; r1 存放 x[i] 地址,加载到 r0
mul r1, r0, r0 ; r1 = x[i]^2
mul r1, r1, r0 ; r1 = x[i]^3 -> 对应 numer 初值
...
st addr[r2], r0 ; 计算完成后,存入 result[i]
理解:
ld:加载数组元素到寄存器mul:计算幂次(如 x^3)st:把最终结果存回result[i]
并行执行的潜在优化
这个程序是 “数据并行” 的典型例子:
- 每个数组元素的计算互不依赖 → 可以用多核 CPU/GPU 并行处理
- 外层
for (i)循环完全可以分给多个线程或 SIMD 指令 - 内层
for (j)由于迭代依赖前一项,暂时无法并行
总结:
- C++ 程序 → Taylor 展开计算 sin(x)
- 汇编 → 展示了加载、乘法、存储流程
- 并行优化 → 数据级并行可显著提升性能
#include <iostream>
#include <cmath> // 仅用于验证结果
// 使用 Taylor 展开计算 sin(x) 对数组每个元素进行计算
void sinx(int N, int terms, float* x, float* result) {
for (int i = 0; i < N; i++) { // 遍历数组每个元素
float value = x[i]; // 初始值 sin(x) 的第 0 项
float numer = x[i] * x[i] * x[i]; // 分子初始值 x^3
int denom = 6; // 分母初始值 3! = 6
int sign = -1; // 符号轮换,从负号开始
for (int j = 1; j <= terms; j++) { // Taylor 展开迭代 terms 次
value += sign * numer / denom; // 加上当前项
numer *= x[i] * x[i]; // 更新分子 x^(2j+3)
denom *= (2*j+2)*(2*j+3); // 更新分母 (2j+2)*(2j+3)
sign *= -1; // 符号翻转
}
result[i] = value; // 存储结果
}
}
int main() {
const int N = 5; // 数组长度
const int terms = 5; // Taylor 展开项数
float x[N] = {0.0f, 0.5f, 1.0f, 1.5f, 3.14f}; // 输入数组
float result[N]; // 输出结果数组
// 调用函数
sinx(N, terms, x, result);
// 打印结果,并与标准库 sin 函数比较
for (int i = 0; i < N; i++) {
std::cout << "x = " << x[i]
<< ", sinx = " << result[i]
<< ", std::sin = " << std::sin(x[i])
<< std::endl;
}
return 0;
}
程序解析
- 外层循环
i
遍历输入数组的每个元素,每个元素可以独立计算 → 数据级并行可行。 - 内层循环
j
计算 Taylor 展开式的每一项:
sin(x)≈x−x33!+x55!−… \sin(x) \approx x - \frac{x^3}{3!} + \frac{x^5}{5!} - \dots sin(x)≈x−3!x3+5!x5−… - 符号控制
sign *= -1保证交替加减。 - 分子和分母更新
- 分子:乘以
x[i]*x[i] - 分母:累乘
(2*j+2)*(2*j+3)→ 阶乘增长
- 分子:乘以
- 存储结果
最终的value存入result[i]。
可优化点
- 并行化:外层循环可用 OpenMP、CUDA、ISPC 等并行执行。
- SIMD:对数组做向量化计算,提高 CPU 浮点吞吐量。
- 减少浮点操作:复用分子和分母,避免重复计算幂和阶乘。
┌─────────────────────────────────┐
│ │ ┌─────────────────────────┐ ┌─────────────────────────┐ ┌─────────────────────────┐
│ ┌─────────────────────┐ │ │PC -> ld r0, addr[r1] │ │ ld r0, addr[r1] │ │ ld r0, addr[r1] │
│ │ Fetch/ │ │ │ │ │ │ │ │
│ │ Decode │ │ │ mul r1, r0, r0 │ │PC -> mul r1, r0, r0 │ │ mul r1, r0, r0 │
│ └─────────────────────┘ │ │ │ │ │ │ │
│ ┌─────────────────────┐ │ │ mul r1, r1, r0 │ │ mul r1, r1, r0 │ │PC -> mul r1, r1, r0 │
│ │ ALU │ │ │ │ │ │ │ │
│ │ (Execute) │ │ │ ... │ │ ... │ │ ... │
│ └─────────────────────┘ │ │ │ │ │ │ │
│ │ │ ... │ │ ... │ │ ... │
│ ┌─────────────────────┐ │ │ │ │ │ │ │
│ │ Execution │ │ │ ... │ │ ... │ │ ... │
│ │ Context │ │ │ │ │ │ │ │
│ │ ┌───────┐┌───────┐ │ │ │ ... │ │ ... │ │ ... │
│ │ ├───────┤├───────┤ │ │ │ │ │ │ │ │
│ │ ├───────┤├───────┤ │ │ │ ... │ │ ... │ │ ... │
│ │ ├───────┤├───────┤ │ │ │ │ │ │ │ │
│ │ └───────┘└───────┘ │ │ │ ... │ │ ... │ │ ... │
│ │ │ │ │ │ │ │ │ │
│ └─────────────────────┘ │ │ st addr[r2], r0 │ │ st addr[r2], r0 │ │ st addr[r2], r0 │
│ │ │ │ │ │ │ │
└─────────────────────────────────┘ └─────────────────────────┘ └─────────────────────────┘ └─────────────────────────┘
┌─────────────────────────────────┐
│ │ ┌─────────────────────┐
│ ┌───────────┐ ┌───────────┐ │ │ ld r0, addr[r1] │
│ │ Fetch/ │ │ Fetch/ │ │ │ │
│ │ Decode │ │ Decode │ │ │ mul r1, r0, r0 │
│ └───────────┘ └───────────┘ │ │ │
│ ┌───────────┐ ┌───────────┐ │ │ mul r1, r1, r0 │
│ │ ALU │ │ ALU │ │ │ │
│ │ (Execute) │ │ (Execute) │ │ │ ... │
│ └───────────┘ └───────────┘ │ │ │
│ │ │ ... │
│ ┌─────────────────────┐ │ │ │
│ │ Execution │ │ │ ... │
│ │ Context │ │ │ │
│ │ ┌───────┐┌───────┐ │ │ │ ... │
│ │ ├───────┤├───────┤ │ │ │ │
│ │ ├───────┤├───────┤ │ │ │ ... │
│ │ ├───────┤├───────┤ │ │ │ │
│ │ └───────┘└───────┘ │ │ │ ... │
│ │ │ │ │ │
│ └─────────────────────┘ │ │ st addr[r2], r0 │
│ │ │ │
└─────────────────────────────────┘ └─────────────────────┘
ld r0, addr[r1]
mul r1, r0, r0
mul r1, r1, r0
Note: No ILP exists in this region of the program
1. 简单处理器(单发射)
你的示意图展示了一个 非常简单的处理器:
- 特点:每个时钟周期执行一条指令(单发射)。
- 执行流程:
- Fetch/Decode:取指令并解码。
- ALU (Execute):执行指令。
- Execution Context:更新寄存器/内存状态。
- 示例指令流:
ld r0, addr[r1] // 从内存加载 x[i] 到 r0 mul r1, r0, r0 // r1 = r0 * r0 mul r1, r1, r0 // r1 = r1 * r0 st addr[r2], r0 // 将结果存回内存 - 特点分析:
- 每条指令必须等待上一条完成才能执行。
- 流水线深度浅,吞吐量低。
- 没有利用 指令级并行性(ILP)。
2. 超标量处理器(Superscalar)
- 特点:每个时钟周期可以尝试执行多条指令(2条或更多),前提是这些指令没有数据依赖。
- 执行流程:
- Fetch/Decode 两条指令并行。
- ALU 执行两条指令并行。
- 寄存器更新/存储也可以并行。
- 示意图:
时钟1: ld r0, addr[r1] | mul r1, r0, r0 时钟2: mul r1, r1, r0 | ... - 优势:
- 能利用 指令级并行性(ILP) 提高吞吐量。
- 处理器面积/功耗相同情况下,性能提升显著。
- 限制:
- 如果指令有数据依赖(例如
mul r1, r0, r0必须等ld r0, addr[r1]完成),那么 ILP 无法发挥作用。 - 如你的示例中:
这里三条指令都存在依赖,没有 ILP 可利用,即使是超标量处理器,也无法并行执行这些指令。ld r0, addr[r1] mul r1, r0, r0 mul r1, r1, r0
- 如果指令有数据依赖(例如
3. 总结
| 类型 | 每周期指令数 | ILP 利用情况 | 特点 |
|---|---|---|---|
| 单发射处理器 | 1 | 无 | 简单,吞吐量低 |
| 超标量处理器 | 2 或更多 | 数据无依赖时有效 | 提高吞吐量,硬件复杂 |
- 关键概念:
- ILP(Instruction-Level Parallelism):利用指令之间的独立性,同时执行多条指令。
- 数据依赖会限制 ILP。
- 超标量处理器并不是对所有代码都加速,顺序依赖强的代码无法并行化。
System Bus (External) ┌─────────────┐
◀─────────────────◀─┬─────────────▶ │ L2 Cache │
│ └─────▲───────┘
│ │
│ │ Cache Bus
│ │
┌─────▼────────────────────────▼─────────────────┐◀─────────────────────────────────────────────────────────────┐
│ Bus Interface Unit │◀──────────────────────────────────────────┐ │
└──────────▲─────────────────────────▲───────────┘ │ │
│ │ │ │
│ │ │ │
┌─────────▼─────────────┬───────────▼───────────┐ ┌────────────────────────┐ │ │
│Instruction Fetch Unit │Instruction Cache (L1) ◀────▶ Next IP │ │ │
└──────────────┬────────┴───────────────────────┘ │ Unit │ │ │
│ │ │ │ │
│ ├ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ │ │
┌──────────────▼────────────────────────────────┐ │ Branch │ │ │
│ Instruction Decoder ├────▶ Target ◀─────┐ │ │
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ │ │ Buffer │ │ │ │
│ │Simple │ │Simple │ │Complex │ │ │ │ │ │ │
│ │Instuction │ │Instuction │ │Instuction │ │ └────────────────────────┘ From │ │
│ │Decoder │ │Decoder │ │Decoder │ │ ┌────────────────────┐ Integer │ │
│ └─────┬─────┘ └────┬──────┘ └┬──┬──┬──┬─┘ ◀────▶ Microcode │ Unit │ │
└───────┼──────────────┼──────────┼──┼──┼──┼────┘ │ Instruction │ │ ┌─────▼────────┐
│ │ │ │ │ │ │ Sequencer │ │ │ │
│ │ │ │ │ │ └────────────────────┘ │ │ Memory │
┌──────▼──────────────▼──────────▼──▼──▼──▼───┐ │ │ Reorder │
│ Register Alias Table │ │ │ Buffer │
└──────┬──────────────────────────────────────┘ │ │ │
│ │ └───────▲──────┘
│ │ │
│ ┌──────────────────────────────────┐ │ │
├───────▶ Retirement Unit │ ┌────────▼───────┐ │
│ ├──────────────────────────────────┤ │ │ │
┌────────────────────┼───────▶Reorder Buffer (Instruction Pool) │ │ Data Cache │ │
│ │ └───────────────▲──────────────────┘ │ Unit (L1) │ │
│ │ │ │ │ │
│ ▼ │ └─────────┬──────┘ │
│ ┌──────────────────────────────────────▼───────────────────────────────────────────────┐ │ │
│ │ Reservation Station │ │ │
│ └──▲──────────────────────────────┬────────────────────────────────────────────────────┘ │ │
│ │ │ │ │
│ │ │ │ │
│ │ ┌─────────────────────────▼────────────────────────────────────────────┐ │ │
│ │ │ Execution Unit │ │ │
│ │ │ ┌─────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐│ │ │
│ │ │ │ SIMD FP │ │Floating- │ │ Integer │ │ Integer │ │ Memory │◀────────────────────│ │
│ └─┐ │ │ Unit │ │Point Unit│ │ Unit │ │ Unit │ │ Interface││ │ │
│ │ │ │ (FPU) │ │(FPU) │ │ │ │ │ │ Unit ││ │ │
│ │ │ └─────────┘ └──────────┘ └──────────┘ └──────────┘ └──────────┘│ │ │
│ │ │ │ │ │
│ │ └─────────────────────────┬────────────────────────────────────────────┘ │ │
│ │ │ │ │
│ │ │ │ │
│ ┌────┴────────────────────────────▼─────────────────────────────────────────────────────────────────▼───────────────────▼───────┐
└─────┤ Internal Data-Results Buses │
└───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
现代超标量 CPU 内部结构示意图,我帮你用梳理理解一下。这个图主要展示了指令从 取指到执行再到写回 的全过程,以及 CPU 各个模块之间的关系。
1. 系统总线与缓存
- System Bus (外部总线)
- 连接 CPU 与主存或外设。
- 外部访问速度慢,需要缓存提高效率。
- L2 Cache(二级缓存)
- 容量大于 L1,速度慢于 L1。
- 缓存数据和指令,减少对主存的访问。
- Cache Bus(缓存总线)
- CPU 内部访问 L2 Cache 的通路。
2. 指令获取与缓存
- Instruction Fetch Unit(取指单元)
- 从 L1 指令缓存或 L2 Cache 取指令。
- Instruction Cache (L1)
- 一级缓存,存储最近使用的指令,速度最快。
- Next IP Unit / Branch Target Buffer
- 预测下一条要执行的指令地址(IP = Instruction Pointer),处理分支预测。
3. 指令解码
- Instruction Decoder(指令解码器)
- 将取到的机器指令为 CPU 可以执行的微操作(Micro-ops)。
- Simple Instruction Decoder:处理简单指令
- Complex Instruction Decoder:处理复杂指令
- Microcode Instruction Sequencer(微码指令序列器)
- 对复杂指令生成一系列微操作,供执行单元使用。
4. 重命名与乱序执行
- Register Alias Table(寄存器别名表)
- 用于寄存器重命名,避免写后读(WAR)或写后写(WAW)依赖。
- Reorder Buffer / Retirement Unit(重排缓冲/提交单元)
- 指令可以乱序执行,但保证最终 程序语义顺序提交。
- 执行完的指令先放入 Reorder Buffer,按程序顺序写回寄存器和内存。
- Reservation Station(保留站)
- 存放等待执行的微操作,解决数据依赖问题。
- 当所需操作数就绪时,分派到执行单元。
5. 执行单元
- Execution Unit(执行单元):
- SIMD FP Unit(向量浮点单元)
- Floating Point Unit(标量浮点单元)
- Integer Unit(整数单元)
- Memory Interface Unit(内存接口单元)
- 功能:真正执行指令操作,包括算术、逻辑、加载/存储等。
- Internal Data-Results Buses(内部数据结果总线)
- 执行单元的计算结果通过总线传回寄存器或 Reorder Buffer。
6. 内存层次
- Data Cache L1(一级数据缓存)
- 存储最近使用的数据,提高访问速度。
- Memory Reorder Buffer(内存乱序缓冲)
- 协调乱序执行与内存访问顺序,保证程序语义正确。
7. 工作流程总结(指令流水)
- 取指:Instruction Fetch → L1 Instruction Cache → Branch Prediction
- 解码:Instruction Decoder → 微操作序列
- 寄存器重命名:Register Alias Table
- 调度执行:Reservation Station → Execution Units
- 结果写回:Reorder Buffer → 寄存器/内存
- 缓存层次:L1/L2 Cache → 主存
特点:
- 支持乱序执行、超标量、分支预测、寄存器重命名。
- 提高指令级并行性(ILP)和 CPU 吞吐量。
- 保证程序语义顺序与最终结果一致。
┌────────────────────────────────────────────────────────────────────────────────────────────────┐
│ ┌─────────────────────────────────┐ ┌─────────────────────────────────────────────────────┐ │
│ │ │ │ │ │
│ │ ┌───────────┐ ┌───────────┐ │ │ │ │
│ │ │ Fetch/ │ │ Fetch/ │ │ │ │ │
│ │ │ Decode │ │ Decode │ │ │ Data cache │ │
│ │ └───────────┘ └───────────┘ │ │ │ │
│ │ ┌───────────┐ ┌───────────┐ │ │ (a big one) │ │
│ │ │ ALU │ │ ALU │ │ │ │ │
│ │ │ (Execute) │ │ (Execute) │ │ │ │ │
│ │ └───────────┘ └───────────┘ │ └─────────────────────────────────────────────────────┘ │
│ │ │ │
│ │ ┌─────────────────────┐ │ ┌─────────────────────────────────────────────────────┐ │
│ │ │ Execution │ │ │ Out-of-order control logic │ │
│ │ │ Context │ │ └─────────────────────────────────────────────────────┘ │
│ │ │ ┌───────┐┌───────┐ │ │ ┌─────────────────────────────────────────────────────┐ │
│ │ │ ├───────┤├───────┤ │ │ │ Fancy branch predictor │ │
│ │ │ ├───────┤├───────┤ │ │ └─────────────────────────────────────────────────────┘ │
│ │ │ ├───────┤├───────┤ │ │ ┌─────────────────────────────────────────────────────┐ │
│ │ │ └───────┘└───────┘ │ │ │ Memory pre-fetcher │ │
│ │ │ │ │ └─────────────────────────────────────────────────────┘ │
│ │ └─────────────────────┘ │ │
│ │ │ │
│ └─────────────────────────────────┘ │
│ │
└────────────────────────────────────────────────────────────────────────────────────────────────┘
好的,这张图展示的是 多核时代之前的单核处理器(pre multi-core era)架构,我帮你理解一下:
1. 核心思想
- 当时处理器主要 用大量晶体管优化单条指令流(single instruction stream)执行速度。
- 目标:让单线程程序跑得尽可能快,而不是增加多核并行。
- 使用的主要策略:
- 增大缓存(Data Cache)
- 更智能的乱序执行控制(Out-of-order control logic)
- 高级分支预测(Fancy branch predictor)
- 内存预取(Memory pre-fetcher)
- 更高的时钟频率(通过更小的晶体管)
2. 图中模块理解
核心执行单元
- Fetch/Decode(取指/解码)
- 读取指令并为微操作。
- ALU (Execute)(算术逻辑单元/执行单元)
- 执行算术运算、逻辑运算等。
- Execution Context(执行上下文)
- 保存寄存器状态、指令状态、执行结果等。
性能优化模块
- Data Cache(数据缓存)
- 存储最近使用的数据,加快访问速度。
- 越大越好,但占用晶体管面积。
- Out-of-order Control Logic(乱序执行控制逻辑)
- 支持乱序执行,提高指令级并行(ILP)。
- Fancy Branch Predictor(高级分支预测器)
- 提前预测分支跳转,减少流水线停顿。
- Memory Pre-fetcher(内存预取器)
- 预测未来访问的数据,提前加载到缓存中,减少内存等待。
3. 晶体管使用策略
- 增加晶体管 → 可以:
- 扩大 L1/L2 缓存容量
- 做更复杂的乱序控制逻辑
- 做更智能的分支预测
- 支持更多内存预取功能
- 同时 → 晶体管越小 → CPU 时钟频率越高 → 单核速度提升
4. 总结
- 单核时代:追求 单线程性能。
- 重点:用更多晶体管做 缓存、乱序执行、预测和预取。
- 多核出现之前,性能提升依赖 ILP + 高频率 + 大缓存 + 智能硬件机制。
┌─────────────────────────────────┐ ┌─────────────────────────────────┐
│ │ │ │
│ ┌───────────┐ │ │ ┌───────────┐ │
│ │ Fetch/ │ │ │ │ Fetch/ │ │
│ │ Decode │ │ │ │ Decode │ │
│ └───────────┘ │ │ └───────────┘ │
│ ┌───────────┐ │ │ ┌───────────┐ │
│ │ ALU │ │ │ │ ALU │ │
│ │ (Execute) │ │ │ │ (Execute) │ │
│ └───────────┘ │ │ └───────────┘ │
│ │ │ │
│ ┌─────────────────────┐ │ │ ┌─────────────────────┐ │
│ │ Execution │ │ │ │ Execution │ │
│ │ Context │ │ │ │ Context │ │
│ │ ┌───────┐┌───────┐ │ │ │ │ ┌───────┐┌───────┐ │ │
│ │ ├───────┤├───────┤ │ │ │ │ ├───────┤├───────┤ │ │
│ │ ├───────┤├───────┤ │ │ │ │ ├───────┤├───────┤ │ │
│ │ ├───────┤├───────┤ │ │ │ │ ├───────┤├───────┤ │ │
│ │ └───────┘└───────┘ │ │ │ │ └───────┘└───────┘ │ │
│ │ │ │ │ │ │ │
│ └─────────────────────┘ │ │ └─────────────────────┘ │
│ │ │ │
└─────────────────────────────────┘ └─────────────────────────────────┘
多核时代(multi-core era)的处理器架构变化,我帮你做理解:
1. 核心思想
- 多核时代的理念:
- 不再用增加晶体管去做更复杂的单线程加速(如更智能的乱序执行、分支预测、投机执行)。
- 而是用晶体管增加 更多的核心(core)。
- 每个核心可以独立执行一条指令流,多个核心 并行计算不同的数据或任务。
- 举例:处理一个数组中的元素:
- 单核时代:一个核心依次计算所有元素。
- 多核时代:两个核心可以同时计算两个不同的元素。
2. 图示解读
- 图中显示两个核心,每个核心有:
- Fetch/Decode(取指/解码)
- ALU/Execute(算术逻辑执行单元)
- Execution Context(执行上下文)
- 每个核心相比单核“fancy core”更简单:
- 每个核心执行单线程的速度慢一些(例如 0.75 倍)。
- 但因为有两个核心:
- 总性能 ≈ 核心数 × 单核性能 = 2 × 0.75 = 1.5(潜在加速比)。
3. 指令流示意
假设计算 sinx 函数或类似逐元素运算:
- 核心 1 计算 x[i]:
ld r0, addr[r1]
mul r1, r0, r0
mul r1, r1, r0
...
st addr[r2], r0
result[i]
- 核心 2 计算 x[j]:
ld r0, addr[r1]
mul r1, r0, r0
mul r1, r1, r0
...
st addr[r2], r0
result[j]
- 两条指令流 同时执行,相互独立。
- 数据并行(Data Parallelism):每个核心处理不同数组元素。
4. 总结
- 单核时代:
- 晶体管主要用于提升单线程速度(缓存、乱序执行、预测)。
- 单核速度高,但无法利用多核并行。
- 多核时代:
- 晶体管用于增加核心数量。
- 每个核心简单,但可以并行计算。
- 通过增加核心数实现整体性能提升,即 线程级并行(TLP, Thread-Level Parallelism)。
- 关键公式:
总体性能≈核心数×每核心性能 \text{总体性能} \approx \text{核心数} \times \text{每核心性能} 总体性能≈核心数×每核心性能
1⃣ 原始串行程序
void sinx(int N, int terms, float* x, float* result) {
for (int i = 0; i < N; i++) {
float value = x[i];
float numer = x[i] * x[i] * x[i];
int denom = 6; // 3!
int sign = -1;
for (int j = 1; j <= terms; j++) {
value += sign * numer / denom;
numer *= x[i] * x[i];
denom *= (2*j+2)*(2*j+3);
sign *= -1;
}
result[i] = value;
}
}
理解:
- 这是一个纯串行程序,每次只处理数组
x[i]的一个元素。 - 编译器用 GCC 编译后,只会在 一个 CPU 核心上运行。
- 如果我们换成多核的简化处理器(每核速度 0.75 倍),程序仍然是单线程,总体速度反而更慢(0.75x)。
2⃣ 使用 pthreads 表示并行
typedef struct {
int N;
int terms;
float* x;
float* result;
} my_args;
void* my_thread_start(void* thread_arg) {
my_args* args = (my_args*)thread_arg;
sinx(args->N, args->terms, args->x, args->result);
return NULL;
}
void parallel_sinx(int N, int terms, float* x, float* result) {
pthread_t thread_id;
my_args args;
args.N = N/2;
args.terms = terms;
args.x = x;
args.result = result;
pthread_create(&thread_id, NULL, my_thread_start, &args); // 启动线程
sinx(N - args.N, terms, x + args.N, result + args.N); // 主线程处理剩余数据
pthread_join(thread_id, NULL); // 等待子线程完成
}
理解:
- 程序手动把数组分成两半,创建一个线程去处理前半部分,主线程处理后半部分。
- 通过多线程,两个核心可以同时工作,实现 显式的并行。
- 好处:可以利用多核 CPU 提升速度。
3⃣ 数据并行(Data-Parallel)表示
void sinx(int N, int terms, float* x, float* result) {
// 程序员声明循环迭代相互独立
forall (int i from 0 to N-1) {
float value = x[i];
float numer = x[i] * x[i] * x[i];
int denom = 6;
int sign = -1;
for (int j = 1; j <= terms; j++) {
value += sign * numer / denom;
numer *= x[i] * x[i];
denom *= (2*j+2)*(2*j+3);
sign *= -1;
}
result[i] = value;
}
}
理解:
forall表示 循环迭代相互独立,可以安全地同时执行。- 编译器可以利用这个信息,自动生成多线程或者 GPU 并行代码。
- 不需要程序员手动创建线程,更简洁、易维护。
4⃣ 总结对比
| 实现方式 | 并行性 | 程序员负担 | 硬件利用 |
|---|---|---|---|
| 串行程序 | 无 | 低 | 只用一个核心,低效率 |
| pthreads 手动并行 | 有 | 高 | 多核利用,但代码复杂 |
数据并行声明 (forall) | 有 | 中 | 多核或 SIMD 自动利用,易扩展 |
| 核心概念: |
- 串行程序:没有并行性,硬件多核无法利用。
- 显式线程并行:程序员手动分块,利用多核。
- 数据并行:程序员声明独立迭代,编译器自动生成并行代码,代码更清晰。
Four cores: compute four elements in parallel
┌─────────────────────────────────┐ ┌─────────────────────────────────┐
│ │ │ │
│ ┌───────────┐ │ │ ┌───────────┐ │
│ │ Fetch/ │ │ │ │ Fetch/ │ │
│ │ Decode │ │ │ │ Decode │ │
│ └───────────┘ │ │ └───────────┘ │
│ ┌───────────┐ │ │ ┌───────────┐ │
│ │ ALU │ │ │ │ ALU │ │
│ │ (Execute) │ │ │ │ (Execute) │ │
│ └───────────┘ │ │ └───────────┘ │
│ │ │ │
│ ┌─────────────────────┐ │ │ ┌─────────────────────┐ │
│ │ Execution │ │ │ │ Execution │ │
│ │ Context │ │ │ │ Context │ │
│ │ ┌───────┐┌───────┐ │ │ │ │ ┌───────┐┌───────┐ │ │
│ │ ├───────┤├───────┤ │ │ │ │ ├───────┤├───────┤ │ │
│ │ ├───────┤├───────┤ │ │ │ │ ├───────┤├───────┤ │ │
│ │ ├───────┤├───────┤ │ │ │ │ ├───────┤├───────┤ │ │
│ │ └───────┘└───────┘ │ │ │ │ └───────┘└───────┘ │ │
│ │ │ │ │ │ │ │
│ └─────────────────────┘ │ │ └─────────────────────┘ │
│ │ │ │
└─────────────────────────────────┘ └─────────────────────────────────┘
┌─────────────────────────────────┐ ┌─────────────────────────────────┐
│ │ │ │
│ ┌───────────┐ │ │ ┌───────────┐ │
│ │ Fetch/ │ │ │ │ Fetch/ │ │
│ │ Decode │ │ │ │ Decode │ │
│ └───────────┘ │ │ └───────────┘ │
│ ┌───────────┐ │ │ ┌───────────┐ │
│ │ ALU │ │ │ │ ALU │ │
│ │ (Execute) │ │ │ │ (Execute) │ │
│ └───────────┘ │ │ └───────────┘ │
│ │ │ │
│ ┌─────────────────────┐ │ │ ┌─────────────────────┐ │
│ │ Execution │ │ │ │ Execution │ │
│ │ Context │ │ │ │ Context │ │
│ │ ┌───────┐┌───────┐ │ │ │ │ ┌───────┐┌───────┐ │ │
│ │ ├───────┤├───────┤ │ │ │ │ ├───────┤├───────┤ │ │
│ │ ├───────┤├───────┤ │ │ │ │ ├───────┤├───────┤ │ │
│ │ ├───────┤├───────┤ │ │ │ │ ├───────┤├───────┤ │ │
│ │ └───────┘└───────┘ │ │ │ │ └───────┘└───────┘ │ │
│ │ │ │ │ │ │ │
│ └─────────────────────┘ │ │ └─────────────────────┘ │
│ │ │ │
└─────────────────────────────────┘ └─────────────────────────────────┘
Sixteen cores: compute sixteen elements in parallel
┌─────────────────────────────────┐ ┌─────────────────────────────────┐ ┌─────────────────────────────────┐ ┌─────────────────────────────────┐
│ │ │ │ │ │ │ │
│ ┌───────────┐ │ │ ┌───────────┐ │ │ ┌───────────┐ │ │ ┌───────────┐ │
│ │ Fetch/ │ │ │ │ Fetch/ │ │ │ │ Fetch/ │ │ │ │ Fetch/ │ │
│ │ Decode │ │ │ │ Decode │ │ │ │ Decode │ │ │ │ Decode │ │
│ └───────────┘ │ │ └───────────┘ │ │ └───────────┘ │ │ └───────────┘ │
│ ┌───────────┐ │ │ ┌───────────┐ │ │ ┌───────────┐ │ │ ┌───────────┐ │
│ │ ALU │ │ │ │ ALU │ │ │ │ ALU │ │ │ │ ALU │ │
│ │ (Execute) │ │ │ │ (Execute) │ │ │ │ (Execute) │ │ │ │ (Execute) │ │
│ └───────────┘ │ │ └───────────┘ │ │ └───────────┘ │ │ └───────────┘ │
│ │ │ │ │ │ │ │
│ ┌─────────────────────┐ │ │ ┌─────────────────────┐ │ │ ┌─────────────────────┐ │ │ ┌─────────────────────┐ │
│ │ Execution │ │ │ │ Execution │ │ │ │ Execution │ │ │ │ Execution │ │
│ │ Context │ │ │ │ Context │ │ │ │ Context │ │ │ │ Context │ │
│ │ ┌───────┐┌───────┐ │ │ │ │ ┌───────┐┌───────┐ │ │ │ │ ┌───────┐┌───────┐ │ │ │ │ ┌───────┐┌───────┐ │ │
│ │ ├───────┤├───────┤ │ │ │ │ ├───────┤├───────┤ │ │ │ │ ├───────┤├───────┤ │ │ │ │ ├───────┤├───────┤ │ │
│ │ ├───────┤├───────┤ │ │ │ │ ├───────┤├───────┤ │ │ │ │ ├───────┤├───────┤ │ │ │ │ ├───────┤├───────┤ │ │
│ │ ├───────┤├───────┤ │ │ │ │ ├───────┤├───────┤ │ │ │ │ ├───────┤├───────┤ │ │ │ │ ├───────┤├───────┤ │ │
│ │ └───────┘└───────┘ │ │ │ │ └───────┘└───────┘ │ │ │ │ └───────┘└───────┘ │ │ │ │ └───────┘└───────┘ │ │
│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │
│ └─────────────────────┘ │ │ └─────────────────────┘ │ │ └─────────────────────┘ │ │ └─────────────────────┘ │
│ │ │ │ │ │ │ │
└─────────────────────────────────┘ └─────────────────────────────────┘ └─────────────────────────────────┘ └─────────────────────────────────┘
┌─────────────────────────────────┐ ┌─────────────────────────────────┐ ┌─────────────────────────────────┐ ┌─────────────────────────────────┐
│ │ │ │ │ │ │ │
│ ┌───────────┐ │ │ ┌───────────┐ │ │ ┌───────────┐ │ │ ┌───────────┐ │
│ │ Fetch/ │ │ │ │ Fetch/ │ │ │ │ Fetch/ │ │ │ │ Fetch/ │ │
│ │ Decode │ │ │ │ Decode │ │ │ │ Decode │ │ │ │ Decode │ │
│ └───────────┘ │ │ └───────────┘ │ │ └───────────┘ │ │ └───────────┘ │
│ ┌───────────┐ │ │ ┌───────────┐ │ │ ┌───────────┐ │ │ ┌───────────┐ │
│ │ ALU │ │ │ │ ALU │ │ │ │ ALU │ │ │ │ ALU │ │
│ │ (Execute) │ │ │ │ (Execute) │ │ │ │ (Execute) │ │ │ │ (Execute) │ │
│ └───────────┘ │ │ └───────────┘ │ │ └───────────┘ │ │ └───────────┘ │
│ │ │ │ │ │ │ │
│ ┌─────────────────────┐ │ │ ┌─────────────────────┐ │ │ ┌─────────────────────┐ │ │ ┌─────────────────────┐ │
│ │ Execution │ │ │ │ Execution │ │ │ │ Execution │ │ │ │ Execution │ │
│ │ Context │ │ │ │ Context │ │ │ │ Context │ │ │ │ Context │ │
│ │ ┌───────┐┌───────┐ │ │ │ │ ┌───────┐┌───────┐ │ │ │ │ ┌───────┐┌───────┐ │ │ │ │ ┌───────┐┌───────┐ │ │
│ │ ├───────┤├───────┤ │ │ │ │ ├───────┤├───────┤ │ │ │ │ ├───────┤├───────┤ │ │ │ │ ├───────┤├───────┤ │ │
│ │ ├───────┤├───────┤ │ │ │ │ ├───────┤├───────┤ │ │ │ │ ├───────┤├───────┤ │ │ │ │ ├───────┤├───────┤ │ │
│ │ ├───────┤├───────┤ │ │ │ │ ├───────┤├───────┤ │ │ │ │ ├───────┤├───────┤ │ │ │ │ ├───────┤├───────┤ │ │
│ │ └───────┘└───────┘ │ │ │ │ └───────┘└───────┘ │ │ │ │ └───────┘└───────┘ │ │ │ │ └───────┘└───────┘ │ │
│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │
│ └─────────────────────┘ │ │ └─────────────────────┘ │ │ └─────────────────────┘ │ │ └─────────────────────┘ │
│ │ │ │ │ │ │ │
└─────────────────────────────────┘ └─────────────────────────────────┘ └─────────────────────────────────┘ └─────────────────────────────────┘
┌─────────────────────────────────┐ ┌─────────────────────────────────┐ ┌─────────────────────────────────┐ ┌─────────────────────────────────┐
│ │ │ │ │ │ │ │
│ ┌───────────┐ │ │ ┌───────────┐ │ │ ┌───────────┐ │ │ ┌───────────┐ │
│ │ Fetch/ │ │ │ │ Fetch/ │ │ │ │ Fetch/ │ │ │ │ Fetch/ │ │
│ │ Decode │ │ │ │ Decode │ │ │ │ Decode │ │ │ │ Decode │ │
│ └───────────┘ │ │ └───────────┘ │ │ └───────────┘ │ │ └───────────┘ │
│ ┌───────────┐ │ │ ┌───────────┐ │ │ ┌───────────┐ │ │ ┌───────────┐ │
│ │ ALU │ │ │ │ ALU │ │ │ │ ALU │ │ │ │ ALU │ │
│ │ (Execute) │ │ │ │ (Execute) │ │ │ │ (Execute) │ │ │ │ (Execute) │ │
│ └───────────┘ │ │ └───────────┘ │ │ └───────────┘ │ │ └───────────┘ │
│ │ │ │ │ │ │ │
│ ┌─────────────────────┐ │ │ ┌─────────────────────┐ │ │ ┌─────────────────────┐ │ │ ┌─────────────────────┐ │
│ │ Execution │ │ │ │ Execution │ │ │ │ Execution │ │ │ │ Execution │ │
│ │ Context │ │ │ │ Context │ │ │ │ Context │ │ │ │ Context │ │
│ │ ┌───────┐┌───────┐ │ │ │ │ ┌───────┐┌───────┐ │ │ │ │ ┌───────┐┌───────┐ │ │ │ │ ┌───────┐┌───────┐ │ │
│ │ ├───────┤├───────┤ │ │ │ │ ├───────┤├───────┤ │ │ │ │ ├───────┤├───────┤ │ │ │ │ ├───────┤├───────┤ │ │
│ │ ├───────┤├───────┤ │ │ │ │ ├───────┤├───────┤ │ │ │ │ ├───────┤├───────┤ │ │ │ │ ├───────┤├───────┤ │ │
│ │ ├───────┤├───────┤ │ │ │ │ ├───────┤├───────┤ │ │ │ │ ├───────┤├───────┤ │ │ │ │ ├───────┤├───────┤ │ │
│ │ └───────┘└───────┘ │ │ │ │ └───────┘└───────┘ │ │ │ │ └───────┘└───────┘ │ │ │ │ └───────┘└───────┘ │ │
│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │
│ └─────────────────────┘ │ │ └─────────────────────┘ │ │ └─────────────────────┘ │ │ └─────────────────────┘ │
│ │ │ │ │ │ │ │
└─────────────────────────────────┘ └─────────────────────────────────┘ └─────────────────────────────────┘ └─────────────────────────────────┘
┌─────────────────────────────────┐ ┌─────────────────────────────────┐ ┌─────────────────────────────────┐ ┌─────────────────────────────────┐
│ │ │ │ │ │ │ │
│ ┌───────────┐ │ │ ┌───────────┐ │ │ ┌───────────┐ │ │ ┌───────────┐ │
│ │ Fetch/ │ │ │ │ Fetch/ │ │ │ │ Fetch/ │ │ │ │ Fetch/ │ │
│ │ Decode │ │ │ │ Decode │ │ │ │ Decode │ │ │ │ Decode │ │
│ └───────────┘ │ │ └───────────┘ │ │ └───────────┘ │ │ └───────────┘ │
│ ┌───────────┐ │ │ ┌───────────┐ │ │ ┌───────────┐ │ │ ┌───────────┐ │
│ │ ALU │ │ │ │ ALU │ │ │ │ ALU │ │ │ │ ALU │ │
│ │ (Execute) │ │ │ │ (Execute) │ │ │ │ (Execute) │ │ │ │ (Execute) │ │
│ └───────────┘ │ │ └───────────┘ │ │ └───────────┘ │ │ └───────────┘ │
│ │ │ │ │ │ │ │
│ ┌─────────────────────┐ │ │ ┌─────────────────────┐ │ │ ┌─────────────────────┐ │ │ ┌─────────────────────┐ │
│ │ Execution │ │ │ │ Execution │ │ │ │ Execution │ │ │ │ Execution │ │
│ │ Context │ │ │ │ Context │ │ │ │ Context │ │ │ │ Context │ │
│ │ ┌───────┐┌───────┐ │ │ │ │ ┌───────┐┌───────┐ │ │ │ │ ┌───────┐┌───────┐ │ │ │ │ ┌───────┐┌───────┐ │ │
│ │ ├───────┤├───────┤ │ │ │ │ ├───────┤├───────┤ │ │ │ │ ├───────┤├───────┤ │ │ │ │ ├───────┤├───────┤ │ │
│ │ ├───────┤├───────┤ │ │ │ │ ├───────┤├───────┤ │ │ │ │ ├───────┤├───────┤ │ │ │ │ ├───────┤├───────┤ │ │
│ │ ├───────┤├───────┤ │ │ │ │ ├───────┤├───────┤ │ │ │ │ ├───────┤├───────┤ │ │ │ │ ├───────┤├───────┤ │ │
│ │ └───────┘└───────┘ │ │ │ │ └───────┘└───────┘ │ │ │ │ └───────┘└───────┘ │ │ │ │ └───────┘└───────┘ │ │
│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │
│ └─────────────────────┘ │ │ └─────────────────────┘ │ │ └─────────────────────┘ │ │ └─────────────────────┘ │
│ │ │ │ │ │ │ │
└─────────────────────────────────┘ └─────────────────────────────────┘ └─────────────────────────────────┘ └─────────────────────────────────┘
1⃣ 多核处理器:四核与十六核
你画的图展示了多核 CPU 的核心利用情况:
- 四核 CPU:一次可以计算四个数组元素的结果,每个核心独立执行自己的指令流。
- 十六核 CPU:一次可以计算十六个数组元素,每个核心都有独立的 Fetch/Decode/ALU/Execution 单元。
- 原理:
- 核心数增加 → 可以同时处理更多的独立任务。
- 每个核心可能比老式单核复杂核心慢一点(简化设计),但通过并行计算,总体吞吐量提高。
速度分析(假设每核是 0.75x 单核速度):
- 单核:1 × 1 = 1(基准)
- 四核:4 × 0.75 = 3 → 总体速度 3 倍
- 十六核:16 × 0.75 = 12 → 总体速度 12 倍
2⃣ 多核示例:Intel Coffee Lake Core i7 (2017)
- CPU 类型:桌面处理器
- 核心数:6 个核心(hexa-core)
- 应用:多线程程序可以同时利用 6 个核心计算,提高性能
- 特点:每个核心相对独立,具有自己的指令流水线和执行单元
3⃣ GPU 示例:NVIDIA GTX 1080 (2016)
- GPU 核心数:20 个“SM”(Streaming Multiprocessor,流式多处理器)
- 特点:
- 每个 SM 内部又有很多 ALU,可以执行大量线程
- 适合 大规模数据并行计算(如矩阵运算、图像处理、科学计算)
- 对比 CPU:GPU 核心数量多得多,但单个核心较简单,单线程性能低;优势在于高并行吞吐量。
4⃣ 总结多核思路
- 单核时代:所有性能都靠复杂核心提升(Out-of-order、分支预测、缓存优化)
- 多核时代:
- 利用更多简单核心执行独立任务
- 每个核心负责一个或几个独立的数据元素
- 总体吞吐量大幅提升
- CPU vs GPU:
- CPU 核心少,但单核强
- GPU 核心多,但单核弱,适合海量并行
核心理解:
增加核心数 → 可以同时处理更多任务,程序必须表达并行性,才能利用硬件。
┌───────────────────────────────────────────────────┐
│ ┌───────────────────────────┐ │
│ │ Fetch/ │ │
│ │ Decode │ │
│ └───────────────────────────┘ │
│ ┌─────┐ ┌─────┐ ┌─────┐┌─────┐ │
│ │ALU 0│ │ALU 1│ │ALU 2││ALU 3│ │
│ └─────┘ └─────┘ └─────┘└─────┘ │
│ ┌─────┐ ┌─────┐ ┌─────┐┌─────┐ │
│ │ALU 4│ │ALU 5│ │ALU 6││ALU 7│ │
│ └─────┘ └─────┘ └─────┘└─────┘ │
│ │
│ Execution │
│ Context │
│ ┌─────────────────────────────────────────┐ │
│ │ ┌─┬─┬─┬─┬─┬─┬─┬─┐┌─┬─┬─┬─┬─┬─┬─┬─┐ │ │
│ │ │ │ │ │ │ │ │ │ ││ │ │ │ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │ │ │ ││ │ │ │ │ │ │ │ │ │ │
│ │ └─┴─┴─┴─┴─┴─┴─┴─┘└─┴─┴─┴─┴─┴─┴─┴─┘ │ │
│ │ ┌─┬─┬─┬─┬─┬─┬─┬─┐┌─┬─┬─┬─┬─┬─┬─┬─┐ │ │
│ │ │ │ │ │ │ │ │ │ ││ │ │ │ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │ │ │ ││ │ │ │ │ │ │ │ │ │ │
│ │ └─┴─┴─┴─┴─┴─┴─┴─┘└─┴─┴─┴─┴─┴─┴─┴─┘ │ │
│ │ ┌─┬─┬─┬─┬─┬─┬─┬─┐┌─┬─┬─┬─┬─┬─┬─┬─┐ │ │
│ │ │ │ │ │ │ │ │ │ ││ │ │ │ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │ │ │ ││ │ │ │ │ │ │ │ │ │ │
│ │ └─┴─┴─┴─┴─┴─┴─┴─┘└─┴─┴─┴─┴─┴─┴─┴─┘ │ │
│ │ ┌─┬─┬─┬─┬─┬─┬─┬─┐┌─┬─┬─┬─┬─┬─┬─┬─┐ │ │
│ │ │ │ │ │ │ │ │ │ ││ │ │ │ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │ │ │ ││ │ │ │ │ │ │ │ │ │ │
│ │ └─┴─┴─┴─┴─┴─┴─┴─┘└─┴─┴─┴─┴─┴─┴─┴─┘ │ │
│ └─────────────────────────────────────────┘ │
│ │
└───────────────────────────────────────────────────┘
非常好 ,你现在看到的这部分内容实际上是并行计算从“多核并行”进一步发展到“数据并行(SIMD)”的核心思想。下面我帮你完整、清晰地用解释每个要点。
一、Data-parallel(数据并行)表达式
代码:
void sinx(int N, int terms, float* x, float* result) {
// 声明:每次循环迭代都是独立的
forall (int i from 0 to N-1) {
float value = x[i];
float numer = x[i] * x[i] * x[i];
int denom = 6; // 3!
int sign = -1;
for (int j = 1; j <= terms; j++) {
value += sign * numer / denom;
numer *= x[i] * x[i];
denom *= (2*j+2) * (2*j+3);
sign *= -1;
}
result[i] = value;
}
}
含义:
forall表示这个循环的 每个迭代是独立的,可以 并行执行。- 也就是说:
i=0、i=1、i=2… 之间 互不依赖。- 每个循环体计算
sin(x[i])。 - 所以可以同时在多个核心(CPU 或 GPU)上执行。
关键点:
程序员显式告诉编译器:这些循环迭代没有数据依赖,可以并行。
二、并行粒度(Parallelism granularity)
这段代码的并行性是 “跨循环迭代的”(across iterations)。
也就是说:
- 每次迭代做的事情完全相同(计算一个输入值的正弦函数)。
- 所以这类问题属于 数据并行(Data Parallelism)。
- 编译器可以自动把这个循环转化成多线程,甚至 SIMD 指令。
三、进一步优化的思路:SIMD(单指令多数据)
背景:
即使在单个核心内部,我们也可以通过“同时计算多个数据元素”来加速。
关键思想:
一条指令,多个数据(Single Instruction, Multiple Data)
例如:
result[i..i+3] = sin(x[i..i+3])
CPU 不再逐个执行,而是一次执行 4 个(或 8 个、16 个)元素。
硬件原理图解释:
┌───────────────────────────────────────────────────┐
│ ┌───────────────────────────┐ │
│ │ Fetch/Decode │ ← 一条指令 │
│ └───────────────────────────┘ │
│ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │
│ │ALU 0│ │ALU 1│ │ALU 2│ │ALU 3│ ← 同时执行 │
│ └─────┘ └─────┘ └─────┘ └─────┘ │
│ (Execution Context) │
└───────────────────────────────────────────────────┘
- 只有一个指令流(fetch/decode 一次)
- 但同时有多个算术逻辑单元(ALU)
- 每个 ALU 对不同数据执行相同操作
四、举个具体例子(SIMD 实际等价)
假设我们有:
for (int i = 0; i < 8; i++)
y[i] = a[i] + b[i];
在 SIMD 硬件下,CPU 可能一次执行:
// 载入 8 个浮点数
ymm0 = load(a[0..7]);
ymm1 = load(b[0..7]);
// SIMD 加法
ymm2 = add(ymm0, ymm1);
// 存回结果
store(y[0..7], ymm2);
等价于执行 8 次加法,但只需 1 条指令。
五、SIMD vs 多核并行
| 对比点 | 多核并行 (Multi-core) | SIMD 并行 |
|---|---|---|
| 并行单位 | 不同线程(不同指令流) | 不同数据(相同指令流) |
| 指令流数量 | 多条 | 一条 |
| 典型实现 | pthreads, OpenMP, std::thread | SSE, AVX, NEON |
| 适用场景 | 任务级并行(不同工作) | 数据级并行(相同操作) |
| 例子 | 同时计算 sin(x[0]) 和 sin(x[1]) 在不同核上 | 在同一个核上同时计算 sin(x[0…7]) |
总结一句话
“forall” 表示循环迭代相互独立,可并行。
多核并行是“多个指令流同时工作”;
SIMD 是“一个指令流同时处理多个数据”。
两者结合,就是现代高性能 CPU/GPU 的基础。
CMU 15-418/618 并行计算课程(Fall 2018),讲的是如何把一个标量(scalar)程序转换为使用 AVX SIMD 指令的向量化(vectorized)程序。我们来详细解释每一部分的含义。
一、标量版本(Scalar program)
void sinx(int N, int terms, float* x, float* result) {
for (int i = 0; i < N; i++) {
float value = x[i];
float numer = x[i] * x[i] * x[i];
int denom = 6; // 3!
int sign = -1;
for (int j = 1; j <= terms; j++) {
value += sign * numer / denom;
numer *= x[i] * x[i];
denom *= (2*j+2)*(2*j+3);
sign *= -1;
}
result[i] = value;
}
}
含义:
- 这个函数用 Taylor 级数展开 来近似计算
sin(x):
sin(x)≈x−x33!+x55!−x77!+… \sin(x) \approx x - \frac{x^3}{3!} + \frac{x^5}{5!} - \frac{x^7}{7!} + \dots sin(x)≈x−3!x3+5!x5−7!x7+… terms控制展开的阶数(越多越精确)。- 循环逐个计算数组
x[]中每个元素的sin(x)。
特点:
- 标量执行(scalar execution):每次只处理一个
float。 - 所有计算都是通过普通的浮点寄存器完成。
- 伪汇编指令:
说明:每次加载一个ld r0, addr[r1] mul r1, r0, r0 mul r1, r1, r0 ... st addr[r2], r0x[i],计算,再存入结果。
二、向量化版本(Vector program using AVX intrinsics)
代码
#include <immintrin.h>
void sinx(int N, int terms, float* x, float* result) {
float three_fact = 6; // 3!
for (int i = 0; i < N; i += 8) {
__m256 origx = _mm256_load_ps(&x[i]);
__m256 value = origx;
__m256 numer = _mm256_mul_ps(origx, _mm256_mul_ps(origx, origx));
__m256 denom = _mm256_broadcast_ss(&three_fact);
int sign = -1;
for (int j = 1; j <= terms; j++) {
__m256 tmp = _mm256_div_ps(
_mm256_mul_ps(_mm256_set1_ps((float)sign), numer),
denom
);
value = _mm256_add_ps(value, tmp);
numer = _mm256_mul_ps(numer, _mm256_mul_ps(origx, origx));
denom = _mm256_mul_ps(denom, _mm256_set1_ps((2*j+2)*(2*j+3)));
sign *= -1;
}
_mm256_store_ps(&result[i], value);
}
}
含义:
- 使用 AVX 256-bit 向量寄存器(
__m256),一次可以处理 8 个 float。 _mm256_load_ps()从内存加载 8 个连续的float。_mm256_mul_ps()、_mm256_add_ps()等操作都是 8 路并行执行。_mm256_set1_ps()用来广播一个标量到所有 8 个通道中。_mm256_store_ps()把结果一次性写回内存。
特点:
| 对比项 | 标量版本 | 向量版本 |
|---|---|---|
| 每次处理数据量 | 1 个浮点数 | 8 个浮点数 |
| 使用寄存器 | 标量寄存器(32 位) | 向量寄存器(256 位) |
| 指令执行方式 | 顺序执行 | SIMD 并行执行 |
| 汇编示例 | mul r1, r0, r0 | vmulps xmm1, xmm0, xmm0 |
| 性能 | 慢 | 理论上可快 8 倍 |
对应汇编层面:
AVX 版本编译后类似:
vloadps xmm0, addr[r1]
vmulps xmm1, xmm0, xmm0
vmulps xmm1, xmm1, xmm0
...
vstoreps addr[r2], xmm0
vloadps:向量加载 8 个floatvmulps:向量乘法(8 个元素同时相乘)vstoreps:向量存储结果
三、总结与理解
| 概念 | 说明 |
|---|---|
| Scalar program(标量程序) | 一次只处理一个浮点数,使用普通浮点寄存器。 |
| Vector program(向量化程序) | 利用 SIMD(单指令多数据)并行计算,一次处理 8 个 float。 |
| AVX (Advanced Vector Extensions) | Intel 的 SIMD 指令集扩展,支持 256 位寄存器。 |
| 性能收益 | 理论上可提升最多 8 倍性能(取决于内存带宽、分支预测等)。 |
| 关键思想 | 把独立的数据操作批量并行化,而不改变算法逻辑。 |
| 如果你希望我帮你: | |
| 改成 AVX2 + FMA(融合乘加) 优化版本, | |
| 或者生成一个 标量 vs SIMD 运行时间对比 的完整 C++ 测试程序, |
CMU 15-418/618(Parallel Computer Architecture and Programming) 课程讲义中关于 数据并行(data parallelism)与条件分支(conditional execution) 的经典讲解。
下面我们分阶段来详细解释,帮你彻底理解这些图表背后的思想。
第一部分:Data-parallel expression —— 数据并行表达式
原文:
void sinx(int N, int terms, float* x, float* result) {
// declare independent loop iterations
forall (int i from 0 to N-1) {
float value = x[i];
float numer = x[i] * x[i] * x[i];
int denom = 6; // 3!
int sign = -1;
for (int j = 1; j <= terms; j++) {
value += sign * numer / denom;
numer *= x[i] * x[i];
denom *= (2*j+2) * (2*j+3);
sign *= -1;
}
result[i] = value;
}
}
理解:
forall是一个虚构的并行语言关键字(不是 C++ 的一部分)。- 意思是:
“对每个 i 的循环迭代都是独立的(independent iterations)”,
所以编译器可以自动并行化这些循环。
要点:
| 概念 | 含义 |
|---|---|
forall | 明确告诉编译器:循环中每次迭代彼此独立,没有依赖关系。 |
| 独立循环 | 每个 i 的结果只依赖于自己的 x[i],不会影响其他元素。 |
| 编译器优化 | 编译器可以自动生成多核(multi-core)并行代码,以及在每个核内使用 SIMD(Single Instruction, Multiple Data)指令。 |
| 效果 | 一次计算很多数据点,比如用 AVX2/AVX-512 一次处理 8/16/32 个 x[i]。 |
第二部分:What about conditional execution? —— 条件执行怎么办?
原文大意:
如果循环体里有条件语句(
if/else),SIMD 并行执行时会发生什么?
示例代码:
float x = A[i];
if (x > 0) {
float tmp = exp(x, 5.f);
tmp *= kMyConst1;
x = tmp + kMyConst2;
} else {
float tmp = kMyConst1;
x = 2.f * tmp;
}
result[i] = x;
理解:
- 每个
x = A[i]可能不同,有的满足x > 0,有的x <= 0。 - 当这些数据同时放入 8 个 ALU(AVX 8 路并行)中执行时:
- 有的通道需要执行 if 部分;
- 有的通道需要执行 else 部分;
- 它们不能同时执行不同路径!
第三部分:Mask (discard) output of ALU —— 掩码执行机制
原文中提到的 “Mask (discard) output” 是 掩码执行(masked execution) 或称 谓词执行(predicated execution)。
图示理解:
| ALU 通道 | 元素 | 条件结果 |
|---|---|---|
| ALU 1 | A[0] | True |
| ALU 2 | A[1] | True |
| ALU 3 | A[2] | False |
| ALU 4 | A[3] | True |
| ALU 5 | A[4] | False |
| ALU 6 | A[5] | False |
| ALU 7 | A[6] | False |
| ALU 8 | A[7] | False |
| → 只有部分通道执行有效操作,其他通道被“屏蔽(masked out)”。 |
解释:
- SIMD 掩码执行 的机制是:
对于不满足条件的元素,仍然在执行,但结果被丢弃(masked out)。
- 也就是说所有 8 个 ALU 仍然在跑,只是其中一些的计算结果不保留。
- 这就导致了性能浪费:
最坏情况下:8 个 ALU 里只有 1 个在干活,性能只有 1/8。
第四部分:After branch —— 分支执行完成后恢复全速
原文:“After branch: continue at full performance”
意思是:
- 一旦
if/else的两条路径都执行完, - 程序重新回到没有条件的统一路径,
- 所有 ALU 又可以同时工作,
- 性能恢复到峰值(full throughput)。
总结对照表
| 阶段 | 现象 | 解释 |
|---|---|---|
| Data-parallel expression | 使用 forall 并行循环 | 每个迭代独立,编译器可并行化 |
| Conditional execution | if 判断引入控制流分歧 | 不同数据走不同分支,破坏并行 |
| Mask (discard) output | 使用掩码执行 | 仍然执行全部 ALU,但屏蔽部分结果 |
| After branch | 恢复统一路径 | 所有 ALU 重新并行,恢复满速执行 |
举个直观例子
假设我们有 8 个输入:
A = [-1, 2, -3, 4, -5, 6, -7, 8]
执行逻辑:
if (x > 0) {
x = x * 10;
} else {
x = 0;
}
在 SIMD 下:
- 第 2、4、6、8 个元素满足条件;
- 其它为假;
- 所以 GPU/CPU SIMD 单元需要:
- 先执行所有 if 路径(mask 掩码控制谁生效);
- 再执行所有 else 路径;
- 最后合并结果。
性能取决于条件分支的分布。
如果 8 个数据都同一方向(全 True 或全 False),性能最好;
如果 True/False 混杂,就浪费计算能力。
总体理解
这节课想让你理解:
数据并行编程(Data Parallelism)最怕的就是控制流分歧(Control Flow Divergence)。
在 GPU、SIMD、向量指令中尤其明显:
- 分支越多,掩码越稀疏;
- 实际有效的 ALU 越少;
- 性能下降。
因此,优秀的 SIMD / GPU 程序员会尽量避免分支,通过数学形式(如混合、blend、select)重写代码:
x = (x > 0) ? f1(x) : f2(x);
// 可以改写成:
__m256 mask = _mm256_cmp_ps(x, _mm256_set1_ps(0.0f), _CMP_GT_OQ);
x = _mm256_blendv_ps(f2(x), f1(x), mask);
这样就没有真正的分支,所有通道都在工作,性能最高。
一、Terminology — 基本术语
▪ Instruction stream coherence(指令流一致性 / 指令流相干性)
- 定义:
在 SIMD(单指令多数据)执行中,所有数据元素同时执行相同的指令序列。
换句话说,所有 ALU(算术逻辑单元)都在执行同一条指令,只是作用在不同的数据上。 - 要点:
- “指令流一致” 意味着不同的数据路径上执行完全相同的操作。
- 这是 高效利用 SIMD 资源的必要条件。
否则某些通道(lane)会空闲(即我们在之前学的“mask out”现象)。
- 反例:
如果不同元素因为条件判断(if/else)而执行不同的指令路径 → 指令流不一致(divergent execution) → SIMD 效率下降。
▪ Divergent execution(发散执行)
- 定义:
也叫“指令流发散”,指在 SIMD 组中,不同元素需要执行不同指令的情况。 - 影响:
- 破坏 SIMD 执行的统一性;
- 部分 ALU 必须停下来等待其他路径执行;
- 实际并行效率可能只有理论峰值的 1/4、1/8、甚至 1/32。
▪ 注意区别:
不要把 “instruction stream coherence(指令流一致性)” 和 “cache coherence(缓存一致性)” 搞混。
- 前者讨论的是 “是否执行同一条指令”;
- 后者讨论的是 “多核间共享内存数据是否保持一致”。
二、SIMD execution on modern CPUs — 现代 CPU 上的 SIMD 执行
▪ SSE 与 AVX 指令集
| 指令集 | 向量宽度 | 能并行处理的数据 |
|---|---|---|
| SSE | 128 位 | 4×32-bit float 或 2×64-bit double |
| AVX | 256 位 | 8×32-bit float 或 4×64-bit double |
| AVX-512 | 512 位(部分 CPU) | 16×32-bit float 或 8×64-bit double |
| 所以:一个 AVX 指令可以同时处理 8 个 float 运算。 |
▪ 指令来源
SIMD 指令由编译器生成,有三种方式:
- 显式请求(explicit SIMD)
- 程序员直接写 SIMD 内在函数(intrinsics),如
_mm256_add_ps。 - 或者在编译器里启用自动向量化(例如
-O3 -march=native)。
- 程序员直接写 SIMD 内在函数(intrinsics),如
- 语言语义传达(parallel semantics)
- 用并行语言结构(如
forall、#pragma omp simd)告诉编译器:循环可并行。
- 用并行语言结构(如
- 依赖分析自动推断(dependency analysis)
- 编译器自动分析循环依赖关系;
- 如果确认各迭代独立,则自动生成 SIMD 指令;
- 但这是困难问题:即使最好的编译器,对复杂 C/C++ 代码也常常分析失败。
▪ 术语:“explicit SIMD” 显式 SIMD
意味着 SIMD 并行化在编译期完成,编译后你能在汇编中看到对应的 SIMD 指令。
例如:
vaddps ymm0, ymm1, ymm2 // AVX 加法
vmulps ymm3, ymm4, ymm5 // AVX 乘法
vstoreps [addr], ymm0 // 存储结果
三、SIMD execution on GPUs — GPU 上的 SIMD 执行
▪ “Implicit SIMD” (隐式 SIMD)
GPU 和 CPU 不同:
- 编译器只生成标量代码(scalar instructions);
- 但是 硬件执行时自动同时运行多个实例。
即:
execute(my_function, N);
表示执行 my_function N 次(每个实例对应不同的数据元素)。
硬件负责:
- 让多个实例在同一时间执行相同指令;
- 在不同数据上同时计算(SIMD 化)。
也就是说,GPU 的硬件架构本身是数据并行的(data-parallel)。
▪ GPU 的 SIMD 宽度
- 大多数现代 GPU 的 SIMD 宽度为 8 到 32。
- 这意味着:
- 一个“warp”(在 NVIDIA 中)包含 32 个线程;
- 每次执行一条相同指令;
- 如果分支发散,只执行部分线程;
- 最差情况下,性能可能仅为峰值的 1/32。
四、具体硬件例子
Intel Core i7 (示例配置)
- 4 个物理核心;
- 每个核心有 8 个 SIMD ALU;
- 使用 AVX 指令集;
- 所以总共可以同时并行计算约
4 × 8 = 32个浮点元素。
NVIDIA GTX 480 (GPU)
- 15 个计算核心;
- 每个核心有 32 个 SIMD ALU;
- 理论峰值性能:1.3 TFLOPS(每秒 1.3 万亿次浮点运算);
- 这就是 GPU 能爆炸性提升数据并行任务性能的原因。
五、Summary — 并行执行总结
| 并行形式 | 描述 | 控制方式 |
|---|---|---|
| Multi-core 多核 | 同时运行多个不同的线程(不同的指令流) | 程序员显式创建线程(如 pthread、OpenMP) |
| SIMD 向量化 | 同一条指令同时作用于多个数据 | 通常由编译器(或硬件)实现 |
| Superscalar 超标量 | 同一条指令流中的多条指令在硬件中动态并行执行 | 由 CPU 自动实现,程序员不可见 |
重点区别表
| 类型 | 并行层级 | 谁控制? | 是否执行相同指令流? |
|---|---|---|---|
| 多核(Multi-core) | 线程级 | 程序员 | 不同核心可执行不同程序 |
| SIMD | 数据级 | 编译器/硬件 | 所有 ALU 执行同一指令 |
| 超标量(Superscalar) | 指令级 | CPU 硬件 | 同一线程内部并行执行多条指令 |
总结一句话
SIMD 并行要求“指令流一致”,多核并行不要求。
GPU 采用隐式 SIMD,CPU 采用显式 SIMD。
发散(divergence)导致 SIMD 执行效率下降。
而多核并行因为每个核能独立执行不同程序,不受此限制。
┌─────────────────────────────────────────────────────┐
│ ┌───────────────────────────┐ ┌───────────┐ │
│ │ Fetch/ │ │ │ │
│ │ Decode │ │ L1 cache │ │
│ └───────────────────────────┘ │ │ │
│ ┌─────┐ ┌─────┐ ┌─────┐┌─────┐ │ (32 KB) │ │
│ │ALU 0│ │ALU 1│ │ALU 2││ALU 3│ │ │ │
│ └─────┘ └─────┘ └─────┘└─────┘ └───────────┘ │
│ ┌─────┐ ┌─────┐ ┌─────┐┌─────┐ │ ┌────────────┐
│ │ALU 4│ │ALU 5│ │ALU 6││ALU 7│ ┌───────────┐ │ │ │
│ └─────┘ └─────┘ └─────┘└─────┘ │ │ │ │ │
│ ┌───────────────────────────────────┐│ │ │ │ │
│ │ Execution ││ L2 cache │ │ │ │
Core 1 │ │ Context ││ │ │ │ │
│ │ ││ (256 KB) │ │ │ │ ┌────────────────┐
│ │┌─┬─┬─┬─┬─┬─┬─┬─┐ ┌─┬─┬─┬─┬─┬─┬─┬─┐││ │ │ │ │ │ │
│ │├─┼─┼─┼─┼─┼─┼─┼─┤ ├─┼─┼─┼─┼─┼─┼─┼─┤││ │ │ │ │ │ │
│ │├─┼─┼─┼─┼─┼─┼─┼─┤ ├─┼─┼─┼─┼─┼─┼─┼─┤││ │ │ │ │ │ │
│ │├─┼─┼─┼─┼─┼─┼─┼─┤ ├─┼─┼─┼─┼─┼─┼─┼─┤│└───────────┘ │ │ │ │ │
│ │└─┴─┴─┴─┴─┴─┴─┴─┘ └─┴─┴─┴─┴─┴─┴─┴─┘│ │ │ │ │ │
│ └───────────────────────────────────┘ │ │ │ │ │
└─────────────────────────────────────────────────────┘ │ │ │ │
● │ │ │ │
│ │ 25 GB/sec │ Memory │
│ │ │ │
● │ ◀══════════════▶│ DDR3 DRAM │
│ L3 cache │ │ │
│ │ │ (Gigabytes) │
● │ (8 MB) │ │ │
│ │ │ │
┌─────────────────────────────────────────────────────┐ │ │ │ │
│ ┌───────────────────────────┐ ┌───────────┐ │ │ │ │ │
│ │ Fetch/ │ │ │ │ │ │ │ │
│ │ Decode │ │ L1 cache │ │ │ │ └────────────────┘
Core N │ └───────────────────────────┘ │ │ │ │ │
│ ┌─────┐ ┌─────┐ ┌─────┐┌─────┐ │ (32 KB) │ │ │ │
│ │ALU 0│ │ALU 1 │ALU 2││ALU 3│ │ │ │ │ │
│ └─────┘ └─────┘ └─────┘└─────┘ └───────────┘ │ │ │
│ ┌─────┐ ┌─────┐ ┌─────┐┌─────┐ │ │ │
│ │ALU 4│ │ALU 5│ │ALU 6││ALU 7│ ┌───────────┐ │ │ │
│ └─────┘ └─────┘ └─────┘└─────┘ │ │ │ │ │
│ ┌───────────────────────────────────┐│ │ │ │ │
│ │ Execution ││ L2 cache │ │ └────────────┘
│ │ Context ││ │ │
│ │ ││ (256 KB) │ │
│ │┌─┬─┬─┬─┬─┬─┬─┬─┐ ┌─┬─┬─┬─┬─┬─┬─┬─┐││ │ │
│ │├─┼─┼─┼─┼─┼─┼─┼─┤ ├─┼─┼─┼─┼─┼─┼─┼─┤││ │ │
│ │├─┼─┼─┼─┼─┼─┼─┼─┤ ├─┼─┼─┼─┼─┼─┼─┼─┤││ │ │
│ │├─┼─┼─┼─┼─┼─┼─┼─┤ ├─┼─┼─┼─┼─┼─┼─┼─┤│└───────────┘ │
│ │└─┴─┴─┴─┴─┴─┴─┴─┘ └─┴─┴─┴─┴─┴─┴─┴─┘│ │
│ └───────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
这段内容来自 CMU 15-418/618(并行计算导论)课程,主要讲的是 内存访问(Part 2: accessing memory),重点是“延迟(latency)”、“带宽(bandwidth)”、“缓存(cache)”、“预取(prefetching)”和“多线程隐藏延迟(multithreading to hide latency)”。
我来帮你分段详细解释成理解:
一、基本术语:延迟与带宽
Memory latency(内存延迟)
- 指 一次内存请求被服务的时间(从发出加载请求到数据到达处理器)。
- 举例:访问一次主内存可能需要 100 个 CPU 周期(约 100 纳秒)。
- 所以延迟 = 时间。
Memory bandwidth(内存带宽)
- 指 单位时间内 内存系统能向处理器传输数据的速率。
- 举例:20 GB/s 表示每秒钟可传输 20 GB 的数据。
- 所以带宽 = 速率。
类比:
延迟好比「取快递的路程时间」;带宽好比「一次能搬多少快递」。
二、Stalls(停顿)
- 当处理器 无法继续执行下一条指令,因为它依赖于尚未完成的指令结果,就会“stall”(停顿)。
- 访问内存是最常见的停顿原因。
示例:
ld r0, mem[r2] // 从内存加载 mem[r2]
ld r1, mem[r3] // 从内存加载 mem[r3]
add r0, r0, r1 // 依赖前两条指令结果
add不能执行,直到前两条加载完成。- 加载内存可能花费上百周期,CPU 就必须空等。
所以:
内存访问延迟 → 数据依赖 → CPU 停顿。
三、为什么需要缓存(Cache)
现代处理器层级:
L1 cache: 32 KB (每个核心)
L2 cache: 256 KB (每个核心)
L3 cache: 8 MB (共享)
主内存: 几 GB
- 缓存是小而快的内存,存放“最近访问过的数据”。
- L1/L2 缓存 是每个核心独有的(速度快,容量小)。
- L3 缓存 通常是所有核心共享的(速度较慢,容量大)。
- 主内存(DRAM) 容量大但速度慢。
作用:
1⃣ 降低延迟(latency)
2⃣ 提高带宽(bandwidth)
3⃣ 减少 stall 的发生
四、预取(Prefetching)
Prefetching = 预先加载数据到缓存中,让 CPU 用的时候已经在缓存里。
CPU 的预取逻辑:
- 分析程序的访问模式(如连续访问数组)
- 预测下一个要访问的地址
- 提前发出内存加载请求
- 当指令真正执行时,数据已经在 cache 中了 → cache hit!
示例:
predict value of r2, initiate load
predict value of r3, initiate load
...
ld r0, mem[r2]
ld r1, mem[r3]
add r0, r0, r1
优点:
- 隐藏内存访问延迟(数据提前到达)
- 提高缓存命中率
缺点: - 如果预测错了,会:
- 浪费带宽
- 污染缓存(把有用数据挤掉)
五、多线程隐藏延迟(Multithreading hides stalls)
即使有预取,有时仍然有延迟 → 解决方法是:
在一个核心上交错执行多个线程(interleaving threads)。
原理:
- 当一个线程在等数据(stalled),核心就切换去执行另一个线程。
- 这样 ALU 永远有活干,不用空等。
注意: - 多线程并不会让单个内存访问更快(延迟不变),
- 但它能隐藏延迟(让 CPU 不闲着)。
总结一览表
| 概念 | 含义 | 目标 | 类比 |
|---|---|---|---|
| Latency(延迟) | 一次访问耗时 | 减少等待 | 跑一趟取快递的时间 |
| Bandwidth(带宽) | 每秒传输多少数据 | 提高吞吐 | 一次能搬多少快递 |
| Stall(停顿) | CPU 因等待数据而空转 | 尽量避免 | 人在等快递到手 |
| Cache(缓存) | 临时保存常用数据 | 加快访问 | 在家门口放快递柜 |
| Prefetching(预取) | 提前加载数据 | 隐藏延迟 | 快递提前送到 |
| Multithreading(多线程) | 多线程交替执行 | 隐藏延迟 | 一人等快递时,另一个干活 |
TIME ┌─────────────────────────────────────────────────────────────────────────────────────────┐
│ │
Thread 1 Thread 2 Thread 3 Thread 4 │ ┌───────────────────────────┐ │
┃ Elements 0 … 7 Elements 8 … 15 Elements 16 … 23 Elements 24 … 31 │ │ Fetch/ │ │
┃ ┌─┬─┬─┬─┬─┬─┬─┬─┐ ┌─┬─┬─┬─┬─┬─┬─┬─┐ ┌─┬─┬─┬─┬─┬─┬─┬─┐ ┌─┬─┬─┬─┬─┬─┬─┬─┐ │ │ Decode │ │
┃ └─┴─┴─┴─┴─┴─┴─┴─┘ └─┴─┴─┴─┴─┴─┴─┴─┘ └─┴─┴─┴─┴─┴─┴─┴─┘ └─┴─┴─┴─┴─┴─┴─┴─┘ │ └───────────────────────────┘ │
┃ ┌─────────────────┐ │ ┌─────┐ ┌─────┐ ┌─────┐┌─────┐ │
┃ │┌─┬─┬─┬─┬─┬─┬─┬─┐│ │ │ALU 0│ │ALU 1│ │ALU 2││ALU 3│ │
┃ ││ │ │ │ │ │ │ │ ││ │ └─────┘ └─────┘ └─────┘└─────┘ │
┃ ││ │ │ │ │ │ │ │ ││ │ ┌─────┐ ┌─────┐ ┌─────┐┌─────┐ │
┃ │└─┴─┴─┴─┴─┴─┴─┴─┘│ │ │ALU 4│ │ALU 5│ │ALU 6││ALU 7│ │
┃ └──────Stall──────┘ ┌─────────────────┐ │ └─────┘ └─────┘ └─────┘└─────┘ │
┃ ┃ │┌─┬─┬─┬─┬─┬─┬─┬─┐│ │ ┌───────────────────────────────────┐ ┌───────────────────────────────────┐ │
┃ ┃ ││ │ │ │ │ │ │ │ ││ │ │ Execution │ │ Execution │ │
┃ ┃ ││ │ │ │ │ │ │ │ ││ │ │ Context 1 │ │ Context 2 │ │
┃ ┃ │└─┴─┴─┴─┴─┴─┴─┴─┘│ │ │ │ │ │ │
┃ ▼ └──────Stall──────┘ ┌─────────────────┐ │ │┌─┬─┬─┬─┬─┬─┬─┬─┐ ┌─┬─┬─┬─┬─┬─┬─┬─┐│ │┌─┬─┬─┬─┬─┬─┬─┬─┐ ┌─┬─┬─┬─┬─┬─┬─┬─┐│ │
┃ Runnable ┃ │┌─┬─┬─┬─┬─┬─┬─┬─┐│ │ │├─┼─┼─┼─┼─┼─┼─┼─┤ ├─┼─┼─┼─┼─┼─┼─┼─┤│ │├─┼─┼─┼─┼─┼─┼─┼─┤ ├─┼─┼─┼─┼─┼─┼─┼─┤│ │
┃ ┃ ││ │ │ │ │ │ │ │ ││ │ │├─┼─┼─┼─┼─┼─┼─┼─┤ ├─┼─┼─┼─┼─┼─┼─┼─┤│ │├─┼─┼─┼─┼─┼─┼─┼─┤ ├─┼─┼─┼─┼─┼─┼─┼─┤│ │
┃ ┃ ││ │ │ │ │ │ │ │ ││ │ │├─┼─┼─┼─┼─┼─┼─┼─┤ ├─┼─┼─┼─┼─┼─┼─┼─┤│ │├─┼─┼─┼─┼─┼─┼─┼─┤ ├─┼─┼─┼─┼─┼─┼─┼─┤│ │
┃ ┌─────────────────┐ ┃ │└─┴─┴─┴─┴─┴─┴─┴─┘│ │ │└─┴─┴─┴─┴─┴─┴─┴─┘ └─┴─┴─┴─┴─┴─┴─┴─┘│ │└─┴─┴─┴─┴─┴─┴─┴─┘ └─┴─┴─┴─┴─┴─┴─┴─┘│ │
┃ │┌─┬─┬─┬─┬─┬─┬─┬─┐│ ▼ └──────Stall──────┘ ┌─────────────────┐ │ └───────────────────────────────────┘ └───────────────────────────────────┘ │
┃ ││ │ │ │ │ │ │ │ ││ Runnable ┃ │┌─┬─┬─┬─┬─┬─┬─┬─┐│ │ │
┃ ││ │ │ │ │ │ │ │ ││ ┃ ││ │ │ │ │ │ │ │ ││ │ ┌───────────────────────────────────┐ ┌───────────────────────────────────┐ │
┃ │└─┴─┴─┴─┴─┴─┴─┴─┘│ ┃ ││ │ │ │ │ │ │ │ ││ │ │ Execution │ │ Execution │ │
┃ └─────────────────┘ ┌─────────────────┐ ┃ │└─┴─┴─┴─┴─┴─┴─┴─┘│ │ │ Context 3 │ │ Context 4 │ │
┃ Done! │┌─┬─┬─┬─┬─┬─┬─┬─┐│ ▼ └──────Stall──────┘ │ │ │ │ │ │
┃ ││ │ │ │ │ │ │ │ ││ Runnable ┃ │ │┌─┬─┬─┬─┬─┬─┬─┬─┐ ┌─┬─┬─┬─┬─┬─┬─┬─┐│ │┌─┬─┬─┬─┬─┬─┬─┬─┐ ┌─┬─┬─┬─┬─┬─┬─┬─┐│ │
┃ ││ │ │ │ │ │ │ │ ││ ┃ │ │├─┼─┼─┼─┼─┼─┼─┼─┤ ├─┼─┼─┼─┼─┼─┼─┼─┤│ │├─┼─┼─┼─┼─┼─┼─┼─┤ ├─┼─┼─┼─┼─┼─┼─┼─┤│ │
┃ │└─┴─┴─┴─┴─┴─┴─┴─┘│ ┃ │ │├─┼─┼─┼─┼─┼─┼─┼─┤ ├─┼─┼─┼─┼─┼─┼─┼─┤│ │├─┼─┼─┼─┼─┼─┼─┼─┤ ├─┼─┼─┼─┼─┼─┼─┼─┤│ │
┃ └─────────────────┘ ┌─────────────────┐ ┃ │ │├─┼─┼─┼─┼─┼─┼─┼─┤ ├─┼─┼─┼─┼─┼─┼─┼─┤│ │├─┼─┼─┼─┼─┼─┼─┼─┤ ├─┼─┼─┼─┼─┼─┼─┼─┤│ │
┃ Done! │┌─┬─┬─┬─┬─┬─┬─┬─┐│ ▼ │ │└─┴─┴─┴─┴─┴─┴─┴─┘ └─┴─┴─┴─┴─┴─┴─┴─┘│ │└─┴─┴─┴─┴─┴─┴─┴─┘ └─┴─┴─┴─┴─┴─┴─┴─┘│ │
┃ ││ │ │ │ │ │ │ │ ││ Runnable │ └───────────────────────────────────┘ └───────────────────────────────────┘ │
▼ ││ │ │ │ │ │ │ │ ││ │ │
│└─┴─┴─┴─┴─┴─┴─┴─┘│ └─────────────────────────────────────────────────────────────────────────────────────────┘
└─────────────────┘ ┌─────────────────┐
Done! │┌─┬─┬─┬─┬─┬─┬─┬─┐│
││ │ │ │ │ │ │ │ ││
││ │ │ │ │ │ │ │ ││
│└─┴─┴─┴─┴─┴─┴─┴─┘│
└─────────────────┘
Done!
这整段内容讲的是多线程与吞吐量导向(throughput-oriented)处理器设计的核心思想,尤其是 GPU 相对于 CPU 的结构差异。下面是理解与结构化总结
一、核心思想:多线程的目的——隐藏延迟(Latency Hiding)
多线程(Multi-threading)不是为了让单个线程更快,而是为了让核心(core)在等待时不闲着。
- 当一个线程遇到高延迟操作(比如内存访问)而“停滞(stall)”时,核心可以立刻切换到其他线程继续执行。
- 这样做不能减少延迟(latency),但能隐藏延迟(让 ALU 不空闲),提高整体吞吐量(throughput)。
类比:
你有 4 个顾客(线程),如果一个顾客在等咖啡(内存),你就去帮下一个顾客(线程)。这样咖啡机(ALU)就不会空转。
二、延迟隐藏 vs 延迟减少
| 技术 | 目标 | 示例 |
|---|---|---|
| Prefetching(预取) | 减少等待时间 | 提前加载数据 |
| Multi-threading(多线程) | 隐藏等待时间 | 在等待时切换到别的线程 |
| 多线程 ≠ 提高单线程性能 | ||
| 多线程 = 提高整体吞吐率 |
三、执行上下文(Execution Context)存储权衡
CPU 核心的硬件资源有限(寄存器、上下文存储空间等)。
多线程意味着要为每个线程保存寄存器状态(execution context)。
| 情况 | 每线程资源 | 可同时运行的线程数 | 特性 |
|---|---|---|---|
| 小上下文(轻量线程) | 少 | 多(如 16) | 高延迟隐藏能力 |
| 大上下文(重线程) | 多 | 少(如 4) | 低延迟隐藏能力 |
| 这就是 GPU “小上下文 + 海量线程” 的根本设计理念。 |
四、硬件多线程(Hardware Multi-threading)类型
1⃣ 交错多线程(Interleaved Multi-threading)
- 每个时钟周期选择一个线程执行指令。
- 类似“时间片轮转”。
- 用于隐藏高延迟操作(例如内存访问)。
- 例如:GPU 的 warp 轮换调度机制。
2⃣ 同时多线程(Simultaneous Multi-threading, SMT)
- 每个时钟周期从多个线程中取指令并行执行。
- 扩展自超标量(superscalar)架构。
- 示例:Intel 超线程技术(Hyper-Threading)(每核两个线程)。
五、多线程的收益与代价
| 优点 | 缺点 |
|---|---|
| 更高的 ALU 利用率 | 需要额外硬件存储上下文 |
| 隐藏内存延迟 | 单个线程执行变慢 |
| 充分利用超标量单元 | 需要足够的独立工作量 |
| 更好吞吐率 | 更大工作集 → 缓存命中率下降 → 内存带宽压力增加 |
| 多线程的目标是: |
“让所有 ALU 都一直有事干。”
六、GPU 是极端的吞吐量导向处理器
以 NVIDIA GTX 480 为例:
- 每个核心(SM):
- 32 个 ALU(SIMD 方式执行)
- 128 KB execution contexts(用于保存大量线程状态)
- 同时支持 48 个 warp(每 warp 32 线程)→ 1536 个线程并发
- 整个 GPU:
- 15 个核心 × 1536 线程 ≈ 23,000 并行线程
GPU 的哲学是:
- 15 个核心 × 1536 线程 ≈ 23,000 并行线程
“多线程到极致,就能完全隐藏延迟。”
七、带宽与算力的不匹配 —— GPU 的瓶颈
例子:向量乘法(A[i] × B[i])
- 每次运算要加载 A[i]、B[i] 并存储 C[i] → 12 bytes per multiply
- GTX 480:
- 480 个乘法器 × 1.2 GHz = 5760 亿次乘法/秒
- 如果全速运行,需要 6.4 TB/s 内存带宽
- 实际只有 177 GB/s → 只能达到理论性能的约 3%
虽然只有 3% 效率,但仍比四核 CPU 快 7 倍。
说明 GPU 胜在“并行数大”,而非“单线程快”。
八、带宽瓶颈与算术密度(Arithmetic Intensity)
当线程太多、访问内存频繁时,内存带宽成为瓶颈(即使延迟被隐藏)。
解决方向:
- 提高数据复用率
- 在同一线程中多次使用已加载的数据。
- 在不同线程间共享数据(如共享内存、cache)。
- 提高算术密度(Arithmetic Intensity)
- 同样数量的数据访问,尽量执行更多计算。
- “多算少取”:计算是“免费的”,内存是昂贵的。
九、CPU vs GPU 对比总结
| 特征 | CPU | GPU |
|---|---|---|
| 核心数量 | 少(4~16) | 多(数千线程) |
| 每线程性能 | 强 | 弱 |
| 缓存 | 大(L1/L2/L3) | 小(L1/shared memory) |
| 内存带宽 | 中等(25 GB/s) | 极高(177 GB/s) |
| 延迟应对方式 | 依赖缓存和预取 | 依赖多线程切换 |
| 适用任务 | 控制复杂、逻辑多、分支多 | 数据并行、算术密集 |
十、总结:现代处理器的三大设计理念
- 多核心(Multi-core) —— 空间并行
- SIMD(矢量化) —— 数据并行
- 多线程(Thread-level parallelism) —— 时间并行、隐藏延迟
更多推荐



所有评论(0)