CPU vs GPU 性能模型:理解计算与访存的差异
做算子优化这些年,我最大的感触就是——很多人一上来就调代码,却连瓶颈在哪都没搞清楚。你想想看,连敌人是谁都不知道,怎么打?
所以今天咱们先不急着写代码,先把性能模型搞明白。说白了,就是搞清楚你的算子到底是「算得慢」还是「搬得慢」。
CPU 和 GPU 的「性格」差异
CPU 和 GPU 的设计哲学完全不同。我刚开始接触 GPU 编程时,总觉得 GPU 就是「更强的 CPU」,结果踩了不少坑。
来看个直观的对比:
| 维度 | CPU | GPU |
|---|---|---|
| 核心数量 | 几个到几十个 | 几千个 |
| 核心能力 | 单核极强,能处理复杂逻辑 | 单核很弱,但数量多 |
| 缓存大小 | 大(MB 级别) | 小(KB 级别) |
| 内存带宽 | 几十 GB/s | 几百到几千 GB/s |
| 适合场景 | 串行、复杂控制流 | 并行、简单重复计算 |
嗯,这里要注意:GPU 不是万能的。我见过有人把逻辑很复杂的算法硬塞到 GPU 上跑,结果比 CPU 还慢。为什么?因为 GPU 的每个核心其实很「笨」,它擅长的是成千上万个线程做同一件事。
计算密集 vs 访存密集
这是性能分析里最基础的概念。我习惯把算子分成两类:
- 计算密集:算得多,搬得少。比如矩阵乘法、卷积。
- 访存密集:搬得多,算得少。比如向量加法、数据拷贝。
你可能会问:「分这个有什么用?」
用处大了。我曾经优化一个向量加法算子,花了两天时间调计算指令,结果性能纹丝不动。后来一分析才发现,瓶颈根本不在计算,而在内存带宽上。说白了,你算得再快,数据搬不过来也是白搭。
核心原则:优化前先搞清楚瓶颈在哪。计算密集的算子优化计算,访存密集的算子优化访存。
算术强度:一个关键指标
算术强度(Arithmetic Intensity)这个概念,我建议每个做算子优化的人都刻在脑子里。它的定义很简单:
算术强度 = 总计算量(FLOPs) / 总访存量(Bytes)
单位是 FLOPs/Byte。这个值越大,说明算子越「计算密集」;越小,说明越「访存密集」。
举个例子:
- 矩阵乘法(M=N=K=1024):算术强度大约 256 FLOPs/Byte
- 向量加法(N=1024):算术强度大约 0.25 FLOPs/Byte
差距有多大?差了三个数量级。所以矩阵乘法在 GPU 上能跑出很高的利用率,而向量加法基本被内存带宽锁死。
我的经验:算术强度低于 1 的算子,基本不用考虑计算优化,直接优化访存模式就行。我曾经有个算子算术强度只有 0.3,折腾了半天计算指令,最后改成连续访存,性能直接翻倍。
Roofline Model 入门
Roofline Model 是我做性能分析时最常用的工具。它把性能瓶颈可视化,一目了然。
简单来说,Roofline 模型画了两条线:
- 天花板线:硬件的理论峰值性能(FLOPs/s)
- 带宽线:硬件的理论内存带宽(Bytes/s)
这两条线相交的地方,就是「拐点」。拐点对应的算术强度 = 峰值性能 / 带宽。
如果一个算子的算术强度在拐点左边,说明它受带宽限制;在右边,说明受计算限制。
下面是我画的一个 Roofline 模型示意图:
从图上可以清楚看到:向量加法在带宽限制区,矩阵乘法在计算限制区。优化策略完全不同。
注意:Roofline 模型是理论模型,实际性能还会受指令延迟、缓存命中率、线程调度等因素影响。但它能帮你快速定位瓶颈方向,节省大量调试时间。
如何用 Roofline 指导优化
我一般按这个步骤来:
- 计算算术强度:先算算你的算子有多少计算量,多少访存量。
- 画 Roofline 图:找到硬件的峰值性能和带宽。
- 定位瓶颈:看算子在图上落在哪个区域。
- 针对性优化:带宽限制就优化访存,计算限制就优化计算。
举个例子,我曾经优化一个卷积算子:
- 算术强度算出来是 8 FLOPs/Byte
- 在 GPU 的 Roofline 图上,拐点在 20 FLOPs/Byte 左右
- 所以它落在带宽限制区
- 我改成用共享内存做数据复用,算术强度提升到 25
- 性能提升了 3 倍
你看,方向对了,事半功倍。
小技巧:如果你不确定算子的算术强度,可以用 NVIDIA 的 nvprof 或 Nsight 工具直接看。它会告诉你算子的实际算术强度和 roofline 位置。
总结一下
这一章我们聊了:
- CPU 和 GPU 的差异——一个精一个多
- 计算密集 vs 访存密集——瓶颈在哪
- 算术强度——量化指标
- Roofline Model——可视化分析工具
记住一句话:优化前先定位瓶颈,定位瓶颈先算算术强度。
下一章我们会深入具体算子的性能分析方法,到时候会用到今天讲的概念。