使用 NEON 指令加速级联双二阶 - 它是如何工作的?

Using NEON instructions to speed up cascaded biquads - how it works?

我正在尝试了解如何使用 Neon 扩展为 CMSIS 中的 Arm 处理器优化级联双二阶滤波。 代码定义在 #if defined(ARM_MATH_NEON) here, and documentation is here.

当有超过4个双二阶级联时使用NEON intrinsics。我很困惑,如果将一个 biduaq 的输出作为输入提供给下一个 biduaq,那么如何执行任何类型的并行指令?谁能解释一下在这种和平代码中并行完成的工作?

这是文档中的公式:

y[ n ] = b0 * x[ n ] + d1;
d1 = b1 * x[ n ] + a1 * y[ n ] + d2;
d2 = b2 * x[ n ] + a2 * y[ n ];

让我们通过重命名变量来摆脱可变状态,循环的 2 次迭代:

// Iteration 1
y[ n ] = b0 * x[ n ] + d1_0;
const float d1_1 = b1 * x[ n ] + a1 * y[ n ] + d2_0;
const float d2_1 = b2 * x[ n ] + a2 * y[ n ];

// Iteration 2
y[ n + 1 ] = b0 * x[ n + 1 ] + d1_1;
const float d1_2 = b1 * x[ n + 1 ] + a1 * y[ n + 1 ] + d2_1;
const float d2_2 = b2 * x[ n + 1 ] + a2 * y[ n + 1 ];

如果这样写,显然可以替换变量,并并行计算 2 次迭代,方法如下:

// Rewriting iterations to only use data available before the #1
y[ n ] = b0 * x[ n ] + d1_0;
y[ n + 1 ] = b0 * x[ n + 1 ] + b1 * x[ n ] + a1 * b0 * x[ n ] + d1_0 + d2_0;
const float d1_2 = b1 * x[ n + 1 ] + a1 * y[ n + 1 ] + b2 * x[ n ] + a2 * y[ n ];
const float d2_2 = b2 * x[ n + 1 ] + a2 * y[ n + 1 ];

很确定我搞砸了上面的代数,但我希望你明白了。该方法以更多计算为代价消除了数据依赖性。

该特定实现通过移动向量并进行大量额外计算来进行 4 次迭代而不是 2 次迭代。这是主要的 NEON 循环,带有 HLSL 风格的注释,说明 YnV SIMD 向量的通道发生了什么。

float32x4_t YnV = s;
// YnV.w += t1.w * dV.val[ 0 ].x;
s = vextq_f32( zeroV, dV.val[ 0 ], 3 );
YnV = vmlaq_f32( YnV, t1, s );

// YnV.zw += t2.zw * dV.val[ 0 ].xy;
s = vextq_f32( zeroV, dV.val[ 0 ], 2 );
YnV = vmlaq_f32( YnV, t2, s );

// YnV.yzw += t3.yzw * dV.val[ 0 ].xyz
s = vextq_f32( zeroV, dV.val[ 0 ], 1 );
YnV = vmlaq_f32( YnV, t3, s );

// And finally the all-lanes version without shifts:
// YnV.xyzw += t4.xyzw * XnV.xyzw
YnV = vmlaq_f32( YnV, t4, XnV );

双二阶级联可以通过及时偏移它们来并行化。

如果您一次计算 4 个双二阶,最后一个级联双二阶不会对同一批 4 中前一个双二阶的结果进行运算,而是对前一批 4 中保存的结果进行运算。这将删除每个批次中的依赖项。因此,从第一个双二阶到最后一个双二阶沿对角线传播数据需要 4 个延迟步骤,但是吞吐量每个时间步完成 4 个双二阶,或者比一次计算一个双二阶的吞吐量高 4 倍。