考虑以下代码,其中a
是float
的参数数组,而s
是float
的初始未初始化结果数组:
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)改善此类循环代码的性能?
编辑:我后来发现这个公式/算法被称为“折扣总和”,但是在互联网上找不到其并行版本。
答案 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 + vfmadd
和sₖₖ₊₇
计算出dₖₖ₊₇
。 可能值得为此计算16个sₖ₊₈
和sₖ
fmadd
的块。
同样,用于计算µⁱ * sₖ₊₁₆
的FMA仅使用一半的元素。有了一些复杂的技巧,就可以计算出具有相同数量FMA的两个块(我认为这样做不值得-可以尝试一下)。
为进行比较:纯标量实现需要8个元素的8个加法和8个乘法,并且每个运算都取决于先前的结果。
如果您计算的不是公式,则可以保存一个乘法:
dₖ
此外,在标量版本中,您将具有Fused-Multiple-Adds,而不是先进行加法和乘法运算。结果只会相差sₖ = aₖ₊₁ + µ*sₖ₊₁
。
答案 2 :(得分:1)
如果您指的是向量化,则否,而不是直接。由于此计算使用的是上一次迭代的值来计算下一次迭代,因此它不会被简单地矢量化。
不统一使用s
可能会导致问题。从头开始循环也可能。
如果您的意思仅是SIMD集中的指令,那么也许可以使用它们,但不一定带来巨大的好处,并且编译器通常无论如何都知道如何执行此操作。