如何使用条件有效地向量化多项式计算(屋顶线模型)

Posted

技术标签:

【中文标题】如何使用条件有效地向量化多项式计算(屋顶线模型)【英文标题】:How to efficiently vectorize polynomial computation with condition (roofline model) 【发布时间】:2020-12-04 02:01:24 【问题描述】:

我想对长度在 50 到 3000 之间的向量应用小次数 (2-5) 多项式,并尽可能高效地执行此操作。 示例:例如,我们可以取函数:(1+x^2)^3,当x>3时,当x

一个想法是使用 Eigen: 特征::ArrayXd v; 然后简单地应用一个仿函数: v.unaryExpr([&](double x) return x>3 ? std::pow((1+x*x), 3.00) : 0.00;);

尝试使用 GCC 9 和 GCC 10,我发现这个循环没有被矢量化。我确实手动对其进行了矢量化,只是看到增益比我预期的要小得多(1.5x)。我还用逻辑 AND 指令替换了条件,基本上执行两个分支并在 x

一些注意事项 有多种因素在起作用。首先,我的代码中有 RAW 依赖项(使用内在函数)。我不确定这如何影响计算。我用 AVX2 编写了我的代码,所以我期待 4 倍的增益。我认为这起到了一定的作用,但我不能确定,因为 CPU 有无序处理。另一个问题是我不确定我尝试编写的循环的性能是否受内存带宽的限制。

问题 如何确定内存带宽或管道危害是否影响此循环的实现?我在哪里可以学习更好地矢量化这个循环的技术? Eigenr MSVC 或 Linux 中是否有用于此目的的好工具?我使用的是 AMD CPU,而不是 Intel。

【问题讨论】:

在 Linux 下,perf stat 是否有效,perf list 是否向您展示了您可以在 AMD CPU 上使用的一系列性能计数器事件?但无论如何,明显的问题是使用pow 而不仅仅是手动立方。不要为小整数指数调用pow;您的编译器可能不会将其转换回 2x vmulps(或 vmulpd,因为您说您只期望 AVX 的 4 倍加速?)还要确保启用 FMA,而不仅仅是 AVX2。例如-O3 -march=native,可能还有-ffast-math,看看是否有帮助。 @PeterCordes 感谢您的建议:我会尝试性能。我手动进行了立方体。我确实使用了-O3 -march=native。我正在寻找治疗此类问题的一般方法。 【参考方案1】:

您可以使用 -fno-trapping-math 修复 GCC 错过的优化,这应该是默认设置,因为 -ftrapping-math 甚至不能完全工作。使用该选项可以很好地自动矢量化:https://godbolt.org/z/zfKjjq。

#include <stdlib.h>

void foo(double *arr, size_t n) 
    for (size_t i=0 ; i<n ; i++)
        double &tmp = arr[i];
        double sqrp1 = 1.0 + tmp*tmp;
        tmp = tmp>3 ? sqrp1*sqrp1*sqrp1 : 0;
    

它避免了三元组一侧的乘法,因为它们可能引发 C++ 抽象机不会的 FP 异常。

您希望使用三元以外的立方编写它应该让 GCC 自动矢量化,因为在源代码中没有任何 FP 数学运算是有条件的。但它实际上并没有帮助:https://godbolt.org/z/c7Ms9G GCC 的默认值-ftrapping-math 仍然决定在输入上进行分支以避免所有 FP 计算,可能不会引发 C++ 抽象机会引发的溢出(无穷大)异常。如果输入为 NaN,则无效。这就是我的意思是-ftrapping-math 不起作用。 (相关:How to force GCC to assume that a floating-point expression is non-negative?)


Clang也没有问题:https://godbolt.org/z/KvM9fh 我建议在 FMA 可用时使用 clang -O3 -march=native -ffp-contract=fast 跨语句获取 FMA。

(在这种情况下, -ffp-contract=on 足以在该一个表达式内收缩 1.0 + tmp*tmp,但如果您需要避免例如 Kahan 求和,则不能跨语句收缩。clang 默认显然是 -ffp-contract=off,给出单独的mulpd 和 addpd)


当然,您会希望避免使用小整数指数的std::pow。编译器可能不会将其优化为仅 2 次乘法,而是调用完整的 pow 函数。

【讨论】:

这很有趣:clang 似乎以固定长度 4 进行展开。这种技术有名字吗?我想知道它是否有助于解决 RAW 管道危害? @Gabe:通过交错工作的多个依赖链来隐藏延迟称为“软件流水线”。这里真的没有必要。现代 x86 CPU 的乱序执行可以在 GCC 的完全汇总版本中找到跨循环迭代的指令级并行性,并且有足够大的 ROB + 调度程序来隐藏 FMA (4) + MUL (4) 的关键路径延迟+ MUL (4) + AND(1) = 13 个周期(加上负载延迟可能为 6)。 (对于 Skylake)。请记住,没有循环携带的依赖项(i 除外),因此工作是独立的。 但展开通常有助于一些长时间运行的循环,并减少循环开销。还可以帮助在启动时或停顿后稍微更快地提高到全吞吐量,从而更快地将更多负载加载到管道中。但是如果你的数组长度只有 50 到 3000,更好的测量,特别是如果它们不总是 4 的倍数。Clang 在 by-4 循环之后使用标量清理,所以你最多可以有 15 次标量迭代,因为没有汇总 SIMD 或 128 位 SIMD 清理以更接近。 感谢您的解释。我想我会尝试更好地了解 AMD 的 uProf。我很想知道我离最优(最大可能的 FLOPS)还有多远。 @Gabe:Agner Fog 的优化指南非常好;他有 C++ 和汇编指南。 agner.org/optimize。我已经很久没有阅读它们了,主要是我只是参考他的 microarch PDF 来了解 CPU 内部的底层细节。另请参阅***.com/tags/x86/info 链接的其他性能信息

以上是关于如何使用条件有效地向量化多项式计算(屋顶线模型)的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 Python 和 Numpy 计算 r 平方?

轻量化分割模型-LiteSeg

神经网络模型量化综述(上)

量化感知训练实践:实现精度无损的模型压缩和推理加速

《机器学习系统:设计和实现》以MindSpore为例的学习

PaddleSlim 模型量化 源代码解读