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条指令
实战技巧:我曾经用Roofline模型分析过一个图像处理算法。发现它的运算密度只有2 Ops/Byte,明显是访存瓶颈。后来通过循环分块(Tiling)把密度提升到了15 Ops/Byte,性能直接翻了3倍。

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
实战技巧:我在做网络包处理加速器时,发现协议解析部分的分支预测失误率高达25%。后来改用推测执行+回滚机制,把失误代价从20个周期降到了3个周期。

4.5 三类负载的对比与SVG框架图

三类负载的区别,我用一张图来总结:

工作负载分类与特征对比 计算密集型 • 高运算密度 • 数据重用性好 • 访存模式规律 • 适合SIMD/脉动阵列 • 典型:矩阵乘法 • 典型:卷积运算 • 典型:FFT 加速器重点:计算单元 访存密集型 • 低运算密度 • 随机访问模式 • 带宽敏感 • 缓存命中率低 • 典型:哈希查找 • 典型:数据排序 • 典型:图遍历 加速器重点:存储层次 控制密集型 • 高分支密度 • 分支预测困难 • 不规则数据访问 • 数据依赖复杂 • 典型:协议解析 • 典型:状态机 • 典型:决策树 加速器重点:控制逻辑 核心原则:先分类,后设计。不同负载,不同策略。

4.6 混合负载的处理

现实中的程序很少是纯种类型。大多数是混合的。比如一个视频编码器,运动估计部分是计算密集型,熵编码部分是控制密集型。

遇到混合负载怎么办?我的做法是:

  1. 热点分离:用profiling工具找出最耗时的几个函数
  2. 逐类分析:对每个热点函数做分类
  3. 异构加速:不同部分用不同的加速策略
关键提醒:不要试图用一个加速器搞定所有事情。我曾经见过一个团队,想用一个统一的加速器架构同时处理矩阵乘法和哈希查找,结果两边都没做好。术业有专攻,加速器也一样。

4.7 本章小结

工作负载分析是硬件加速器设计的起点。记住三个核心问题:

  • 算得多吗? → 计算密集型 → 堆计算单元
  • 搬得多吗? → 访存密集型 → 优化存储层次
  • 判断多吗? → 控制密集型 → 优化控制流

搞清楚了这些,你的加速器设计就有了方向。剩下的,就是具体实现的问题了。

专注资料整理