4. 工作负载分析:计算密集型、访存密集型、控制密集型负载的特征提取
做硬件加速器设计这么多年,我最大的体会就是:不懂工作负载,就别谈架构设计。你想想看,连你的对手是谁都不知道,怎么打赢这场仗?
工作负载分析,说白了就是搞清楚你的程序到底在"忙什么"。是忙着算数?忙着搬数据?还是忙着做决策?这三种忙法,对应着三种完全不同的加速器设计思路。
4.1 为什么要把工作负载分类?
我个人习惯,拿到一个新应用,第一件事就是做分类。为什么?因为加速器的本质是"扬长避短"——把最耗时的部分用硬件加速,把不合适的部分留给软件。
举个例子。我在做AI推理加速器时,发现矩阵乘法占了90%以上的时间。这明显是计算密集型。但后来做网络协议处理加速器时,发现大部分时间花在查表和条件判断上。这就是控制密集型了。两种场景,加速器的设计思路完全不同。
4.2 计算密集型负载
计算密集型负载,就是那种"CPU算到冒烟"的场景。它的特征是:算术逻辑单元(ALU)利用率极高,但数据访问模式相对规整。
4.2.1 典型特征
- 高运算密度:每加载一次数据,要做很多次运算。比如矩阵乘法,加载一个元素要做N次乘加。
- 数据重用性好:同一个数据会被反复使用。比如卷积操作,一个权重会被多个输入窗口共用。
- 访存模式规律:通常是连续访问或者步长固定的访问,很少出现随机跳转。
4.2.2 特征提取方法
我建议用两个关键指标来量化:
| 指标 | 定义 | 计算密集型典型值 |
|---|---|---|
| 运算密度(Ops/Byte) | 每字节数据加载对应的运算次数 | > 10 Ops/Byte |
| 算术强度(Arithmetic Intensity) | 总运算量 / 总数据移动量 | > 100 FLOPs/Byte |
| 数据重用距离 | 同一数据两次使用之间的间隔 | < 10条指令 |
4.2.3 加速器设计启示
对于计算密集型负载,加速器的设计重点应该是:
- 大规模并行计算单元:比如SIMD、脉动阵列
- 深流水线:充分利用数据重用性
- 本地存储(Scratchpad):减少对外部存储的访问
4.3 访存密集型负载
访存密集型负载,说白了就是"数据搬运工"。它的特征是:大部分时间花在等待数据上,而不是在计算上。
4.3.1 典型特征
- 低运算密度:加载很多数据,但只做少量运算。比如数据排序、哈希查找。
- 随机访问模式:地址跳转频繁,缓存命中率低。
- 带宽敏感:性能受限于内存带宽,而不是计算能力。
4.3.2 特征提取方法
嗯,这里要注意一个容易踩的坑。很多人只看运算密度,但访存密集型还有一个重要指标——访存模式。
// 示例:两种访存模式的对比
// 模式1:连续访问(访存友好)
for (i = 0; i < N; i++) {
sum += A[i]; // 地址连续,缓存友好
}
// 模式2:随机访问(访存密集型)
for (i = 0; i < N; i++) {
sum += A[random_index[i]]; // 地址随机,缓存失效
}
我曾经在一个数据库加速项目中,发现哈希表的随机访问导致90%的缓存缺失。后来通过硬件预取和乱序调度,才把性能拉回来。
| 指标 | 定义 | 访存密集型典型值 |
|---|---|---|
| 缓存缺失率 | L1/L2缓存访问缺失比例 | > 30% |
| 带宽利用率 | 实际带宽 / 理论峰值带宽 | > 70% |
| 访存延迟隐藏度 | 被计算掩盖的访存延迟比例 | < 20% |
4.4 控制密集型负载
控制密集型负载,就是那种"一直在做决定"的程序。它的特征是:分支多、跳转频繁、数据依赖复杂。
4.4.1 典型特征
- 高分支密度:每几条指令就有一个条件分支。
- 分支预测困难:分支模式随机,预测准确率低。
- 不规则数据访问:指针追逐、链表遍历等。
4.4.2 特征提取方法
我个人习惯用这几个指标来量化控制密集型:
| 指标 | 定义 | 控制密集型典型值 |
|---|---|---|
| 分支密度 | 每100条指令中的分支数 | > 20 |
| 分支预测失误率 | 预测错误的分支比例 | > 15% |
| 控制流复杂度 | 控制流图中的环路数量 | > 10 |
4.5 三类负载的对比与SVG框架图
三类负载的区别,我用一张图来总结:
4.6 混合负载的处理
现实中的程序很少是纯种类型。大多数是混合的。比如一个视频编码器,运动估计部分是计算密集型,熵编码部分是控制密集型。
遇到混合负载怎么办?我的做法是:
- 热点分离:用profiling工具找出最耗时的几个函数
- 逐类分析:对每个热点函数做分类
- 异构加速:不同部分用不同的加速策略
4.7 本章小结
工作负载分析是硬件加速器设计的起点。记住三个核心问题:
- 算得多吗? → 计算密集型 → 堆计算单元
- 搬得多吗? → 访存密集型 → 优化存储层次
- 判断多吗? → 控制密集型 → 优化控制流
搞清楚了这些,你的加速器设计就有了方向。剩下的,就是具体实现的问题了。