有机会使用SIMD加速循环代码吗?

时间:2019-02-13 07:03:32

标签: c++ performance vectorization simd avx2

考虑以下代码,其中afloat的参数数组,而sfloat的初始未初始化结果数组:

s[n - 1] = mu * a[n - 1];
for (int j = n - 2; j >= 0; j--)
    s[j] = mu * (a[j] + s[j + 1]);
return s;

是否有机会使用SIMD(AVX2)改善此类循环代码的性能?

编辑:我后来发现这个公式/算法被称为“折扣总和”,但是在互联网上找不到其并行版本。

3 个答案:

答案 0 :(得分:5)

相关:Is it possible to use SIMD on a serial dependency in a calculation, like an exponential moving average filter?-如果在前n个步骤中存在一个封闭式公式,则可以使用该公式来避免串行依赖性。但我认为情况并非如此。

这看起来像是序列依赖性的前缀和类型,位于带有a[j]的垂直加法的顶部。有多种方法可以加快这种速度,例如加速
O( SIMD_width / log(SIMD_width) )

答案 1 :(得分:2)

有时可以将表达式写成矩阵向量乘积。假设您已经知道sₖ₊₈,则可以使用

sₖsₖ₊₇计算aₖaₖ₊₇
 [ µ  µ² µ³ µ⁴ µ⁵ µ⁶ µ⁷ µ⁸]   [aₖ₊₀     ]
 [ 0  µ  µ² µ³ µ⁴ µ⁵ µ⁶ µ⁷]   [aₖ₊₁     ]
 [ 0  0  µ  µ² µ³ µ⁴ µ⁵ µ⁶]   [aₖ₊₂     ]
 [ 0  0  0  µ  µ² µ³ µ⁴ µ⁵]   [aₖ₊₃     ]
 [ 0  0  0  0  µ  µ² µ³ µ⁴] * [aₖ₊₄     ]
 [ 0  0  0  0  0  µ  µ² µ³]   [aₖ₊₅     ]
 [ 0  0  0  0  0  0  µ  µ²]   [aₖ₊₆     ]
 [ 0  0  0  0  0  0  0  µ ]   [aₖ₊₇+sₖ₊₈]

由于sₖ₊₈在计算时可能会有些延迟,因此将其移出产品是有意义的。可以通过一次广播和一次融合多次相加来计算:

 [ µ  µ² µ³ µ⁴ µ⁵ µ⁶ µ⁷ µ⁸]   [aₖ₊₀]   [ µ⁸]
 [ 0  µ  µ² µ³ µ⁴ µ⁵ µ⁶ µ⁷]   [aₖ₊₁]   [ µ⁷]
 [ 0  0  µ  µ² µ³ µ⁴ µ⁵ µ⁶]   [aₖ₊₂]   [ µ⁶]
 [ 0  0  0  µ  µ² µ³ µ⁴ µ⁵]   [aₖ₊₃]   [ µ⁵]
 [ 0  0  0  0  µ  µ² µ³ µ⁴] * [aₖ₊₄] + [ µ⁴] * sₖ₊₈
 [ 0  0  0  0  0  µ  µ² µ³]   [aₖ₊₅]   [ µ³]
 [ 0  0  0  0  0  0  µ  µ²]   [aₖ₊₆]   [ µ²]
 [ 0  0  0  0  0  0  0  µ ]   [aₖ₊₇]   [ µ ] 

第一个矩阵可以分解为三个矩阵,每个矩阵可以使用一个shuffle和一个FMA来计算:

 [ 1  0  0  0  µ⁴ 0  0  0 ]   [ 1  0  µ² 0  0  0  0  0 ]   [ µ  µ² 0  0  0  0  0  0 ]   [aₖ₊₀]   [ µ⁸]
 [ 0  1  0  0  µ³ 0  0  0 ]   [ 0  1  µ  0  0  0  0  0 ]   [ 0  µ  0  0  0  0  0  0 ]   [aₖ₊₁]   [ µ⁷]
 [ 0  0  1  0  µ² 0  0  0 ]   [ 0  0  1  0  0  0  0  0 ]   [ 0  0  µ  µ² 0  0  0  0 ]   [aₖ₊₂]   [ µ⁶]
 [ 0  0  0  1  µ  0  0  0 ]   [ 0  0  0  1  0  0  0  0 ]   [ 0  0  0  µ  0  0  0  0 ]   [aₖ₊₃]   [ µ⁵]
 [ 0  0  0  0  1  0  0  0 ] * [ 0  0  0  0  1  0  µ² 0 ] * [ 0  0  0  0  µ  µ² 0  0 ] * [aₖ₊₄] + [ µ⁴] * sₖ₊₈
 [ 0  0  0  0  0  1  0  0 ]   [ 0  0  0  0  0  1  µ  0 ]   [ 0  0  0  0  0  µ  0  0 ]   [aₖ₊₅]   [ µ³]
 [ 0  0  0  0  0  0  1  0 ]   [ 0  0  0  0  0  0  1  0 ]   [ 0  0  0  0  0  0  µ  µ²]   [aₖ₊₆]   [ µ²]
 [ 0  0  0  0  0  0  0  1 ]   [ 0  0  0  0  0  0  0  1 ]   [ 0  0  0  0  0  0  0  µ ]   [aₖ₊₇]   [ µ ]

最右边的矩阵向量乘积实际上是一个乘法。

总的来说,对于8个元素,您需要4个FMA,一个乘法和4个混洗/广播($$$$表示此处可以有任何(有限的)内容;或者,如果保证为0,则{{1} }向量可以部分共享。所有向量都以最低有效优先表示,所有乘法都是逐元素的):

µ

如果我正确地分析了它,则多个bₖₖ₊₇ = [aₖ₊₀, aₖ₊₁, aₖ₊₂, aₖ₊₃, aₖ₊₄, aₖ₊₅, aₖ₊₆, aₖ₊₇] * [µ µ µ µ µ µ µ µ ] vmulps bₖₖ₊₇ += [aₖ₊₁, $$$$, aₖ₊₃, $$$$, aₖ₊₅, $$$$, aₖ₊₆, $$$$] * [µ² 0 µ² 0 µ² 0 µ² 0 ] vshufps (or vpsrlq) + vfmadd cₖₖ₊₇ = bₖₖ₊₇ cₖₖ₊₇ += [bₖ₊₂, bₖ₊₂, $$$$, $$$$, bₖ₊₆, bₖ₊₆, $$$$, $$$$] * [µ² µ 0 0 µ² µ 0 0 ] vshufps + vfmadd dₖₖ₊₇ = cₖₖ₊₇ dₖₖ₊₇ += [cₖ₊₄, cₖ₊₄, cₖ₊₄, cₖ₊₄, $$$$, $$$$, $$$$, $$$$] * [µ⁴ µ³ µ² µ 0 0 0 0 ] vpermps + vfmadd sₖₖ₊₇ = dₖₖ₊₇ + [sₖ₊₈, sₖ₊₈, sₖ₊₈, sₖ₊₈, sₖ₊₈, sₖ₊₈, sₖ₊₈, sₖ₊₈] * [µ⁸ µ⁷ µ⁶ µ⁵ µ⁴ µ³ µ² µ ] vbroadcastss + vfmadd 的计算可以交织,从而抵消了延迟。唯一的热路径将是最终的dₖ,以便根据vbroadcastss + vfmaddsₖₖ₊₇计算出dₖₖ₊₇可能值得为此计算16个sₖ₊₈sₖ fmadd的块。 同样,用于计算µⁱ * sₖ₊₁₆的FMA仅使用一半的元素。有了一些复杂的技巧,就可以计算出具有相同数量FMA的两个块(我认为这样做不值得-可以尝试一下)。

为进行比较:纯标量实现需要8个元素的8个加法和8个乘法,并且每个运算都取决于先前的结果。


如果您计算的不是公式,则可以保存一个乘法:

dₖ

此外,在标量版本中,您将具有Fused-Multiple-Adds,而不是先进行加法和乘法运算。结果只会相差sₖ = aₖ₊₁ + µ*sₖ₊₁

答案 2 :(得分:1)

如果您指的是向量化,则否,而不是直接。由于此计算使用的是上一次迭代的值来计算下一次迭代,因此它不会被简单地矢量化。

不统一使用s可能会导致问题。从头开始循环也可能。

如果您的意思仅是SIMD集中的指令,那么也许可以使用它们,但不一定带来巨大的好处,并且编译器通常无论如何都知道如何执行此操作。