Skip to main content
欢迎来到PAWPAW技术文档网站了解更多信息

第2C部分:使用VPU加速

第2B部分一样,第2C部分使用定点算术实现了FIR滤波器。

第2B部分中,我们调用了int32_dot()来计算内积。虽然int32_dot()比我们在C中编写的编译器生成的内积要快得多,但int32_dot()仍然只使用了xcore设备的标量算术单元。

第2C部分int32_dot()替换为对vect_s32_dot()的调用,这是来自lib_xcore_math的库函数之一,它使用VPU来进行计算。

我们将看到使用VPU可以显著提高速度。

来自lib_xcore_math

本阶段使用了lib_xcore_math中的以下操作:

实现

在本部分中,filter_task()rx_frame()tx_frame()第2A部分第2B部分中的相同。

src/part2C/part2C.c
// 将滤波器应用于产生单个输出样本
q1_31 filter_sample(
const q1_31 sample_history[TAP_COUNT])
{
// 与滤波器系数关联的指数
const exponent_t coef_exp = -28;
// 与输入信号关联的指数
const exponent_t input_exp = -31;
// 与输出信号关联的指数
const exponent_t output_exp = input_exp;
// 与累加器关联的指数
const exponent_t acc_exp = input_exp + coef_exp + 30;
// 为了实现正确的输出指数,对滤波器的累加器应用算术右移位
const right_shift_t acc_shr = output_exp - acc_exp;

// 使用lib_xcore_math中的优化函数计算样本历史和滤波器系数之间的64位内积
int64_t acc = vect_s32_dot(&sample_history[0],
&filter_coef[0],
TAP_COUNT,
0,
0);

// 应用右移位操作,将位深度降至32位
return ashr64(acc,
acc_shr);
}

vect_s32_dot()int32_dot()有一个重要的区别,我们需要考虑到这一点。

C_API
int64_t vect_s32_dot(
const int32_t b[],
const int32_t c[],
const unsigned length,
const right_shift_t b_shr,
const right_shift_t c_shr);

int32_dot()的输出是两个int32_t向量的直接内积,而vect_s32_dot()在硬件上有一些额外的操作,一些是由硬件强制要求的,一些是为了块浮点运算而需要的。

ak=0length1(b[k]2b_shrc[k]2c_shr230)a \gets \sum_{k=0}^{\mathtt{length}-1}{\left( \frac{\mathtt{b}[k]}{2^{\mathtt{b\_shr}}} \cdot \frac{\mathtt{c}[k]}{2^{\mathtt{c\_shr}}} \cdot 2^{-30} \right)}
提示

lib_xcore_math文档中,操作的_输出_通常用变量aa表示,而bbcc通常用于操作的输入。上面的aa应该被视为返回值。一些库函数(例如vect_s32_mul())输出向量,因此必须通过参数输出,而不是返回。在这些情况下,aa仍然被认为是输出。

XS3 VPU始终对32位乘积的结果应用30位右移,引入了2302^{-30}因子。b_shrc_shr参数是有符号的算术右移,应用于b[]c[]的各个元素之前进行乘法。

为了理解为什么需要这样做,考虑以下假设情况:

int32_t b[] = {20};
int32_t c[] = {15};
right_shift_t b_shr = 0;
right_shift_t c_shr = 0;
int64_t a = vect_s32_dot(b, c, 1, b_shr, c_shr);

a将是多少?

a=k=0length1(b[k]2b_shrc[k]2c_shr230)=b[0]20c[0]20230=2015230=30030=0\begin{aligned} a &= \sum_{k=0}^{\mathtt{length}-1}{\left( \frac{\mathtt{b}[k]}{2^{\mathtt{b\_shr}}} \cdot \frac{\mathtt{c}[k]}{2^{\mathtt{c\_shr}}} \cdot 2^{-30} \right)} \\ &= \frac{\mathtt{b}[0]}{2^{0}} \cdot \frac{\mathtt{c}[0]}{2^{0}} \cdot 2^{-30} \\ &= 20 \cdot 15 \cdot 2^{-30} \\ &= 300 \gg 30 \\ &= 0 \end{aligned}

如果我们期望a得到300,那结果就不太好了。现在考虑:

int32_t b[] = {20};
int32_t c[] = {15};
right_shift_t b_shr = -14;
right_shift_t c_shr = -16;
int64_t a = vect_s32_dot(b, c, 1, b_shr, c_shr);

现在a将是多少?

a=k=0length1(b[k]2b_shrc[k]2c_shr230)=b[0]214c[0]216230=(20214)(15216)230=327680983040230=32212254720030=300\begin{aligned} a &= \sum_{k=0}^{\mathtt{length}-1}{\left( \frac{\mathtt{b}[k]}{2^{\mathtt{b\_shr}}} \cdot \frac{\mathtt{c}[k]}{2^{\mathtt{c\_shr}}} \cdot 2^{-30} \right)} \\ &= \frac{\mathtt{b}[0]}{2^{-14}} \cdot \frac{\mathtt{c}[0]}{2^{-16}} \cdot 2^{-30} \\ &= (20 \cdot 2^{14}) \cdot (15 \cdot 2^{16}) \cdot 2^{-30} \\ &= 327680 \cdot 983040 \cdot 2^{-30} \\ &= 322122547200 \gg 30 \\ &= 300 \end{aligned}

这次我们使用b_shrc_shr来抵消30位右移。b_shrc_shr选择的特定值并不重要。要求是b_shr + c_shr == -30,并且当b_shrc_shr为负时,不超过b[]c[]的头空间。

回到我们的滤波器,让我们考虑如何修正我们的算术以适应30位右移。

第2A部分中,我们发现当我们计算输入样本和滤波器系数的直接乘积时,累加器的指数只是输入指数和系数指数的总和。但现在,我们不再计算累加器为

P=n=0N1x[kn]b[n]\mathtt{P} = \sum_{n=0}^{N-1} {\mathtt{x}[k-n] \cdot \mathtt{b}[n]}

我们必须将其计算为(在我们对vect_s32_dot()的调用中,b_shr = c_shr = 0

Q=n=0N1(x[kn]b[n]230)=230n=0N1x[kn]b[n]=230P\begin{aligned} \mathtt{Q} &= \sum_{n=0}^{N-1} \left( \mathtt{x}[k-n] \cdot \mathtt{b}[n] \cdot 2^{-30} \right) \\ &= 2^{-30}\,\sum_{n=0}^{N-1} \mathtt{x}[k-n] \cdot \mathtt{b}[n] \\ &= 2^{-30}\cdot \mathtt{P} \end{aligned}

其中Q\mathtt{Q}第2C部分的新累加器变量。

让我们弄清楚我们必须做什么才能获得正确的结果。从我们(简化后的)滤波器方程开始(参见第2A部分):

y[k]2y^=2x^+b^n=0N1x[kn]b[n]=2x^+b^(230230)n=0N1x[kn]b[n]=2x^+b^+30(230n=0N1x[kn]b[n])=2x^+b^+30Q\begin{aligned} \mathtt{y}[k] \cdot 2^{\hat{y}} &= 2^{\hat{x}+\hat{b}} \sum_{n=0}^{N-1} {\mathtt{x}[k-n] \cdot \mathtt{b}[n]} \\ &= 2^{\hat{x}+\hat{b}} \left(2^{30}\cdot 2^{-30}\right) \sum_{n=0}^{N-1} {\mathtt{x}[k-n] \cdot \mathtt{b}[n]} \\ &= 2^{\hat{x}+\hat{b}+30} \cdot \left (2^{-30} \sum_{n=0}^{N-1} {\mathtt{x}[k-n] \cdot \mathtt{b}[n]} \right) \\ &= 2^{\hat{x}+\hat{b}+30} \cdot \mathtt{Q} \\ \end{aligned}

这告诉我们我们的累加器指数acc_exp =Q^=x^+b^+30=P^+30=\hat{Q} = \hat{x}+\hat{b}+30 = \hat{P}+30。 了解了这一点,我们可以像在第2A部分一样计算输出右移量——我们_想要_的输出指数减去我们_已有_的指数:

const exponent_t acc_exp = input_exp + coef_exp + 30;
const right_shift_t acc_shr = output_exp - acc_exp;

有关vect_s32_dot()的完整详细信息,请参阅lib_xcore_math文档

结果(Results)

vect_s32_dot() 函数的内部循环实现有11条指令。而 int32_dot() 函数的内部循环只有4条指令,那么为什么 vect_s32_dot() 的速度要快得多呢?

XS3 VPU 的向量寄存器每个都有8个字长。vect_s32_dot() 使用的 VLMACCR 指令从内存中加载一个向量(8个元素),并同时执行8个乘法(和加法)操作。

因此,虽然在 int32_dot() 中,我们对循环的每次迭代执行一次乘法和加法,但在 vect_s32_dot() 中,我们执行了8次。因此,从简单的角度来看,我们应该期望加速比约为

8 个元素11 条指令(1 个元素4 条指令)1=32112.91 \frac{8\text{ 个元素}}{11\text{ 条指令}} \cdot \left(\frac{1 \text{ 个元素}}{4\text{ 条指令}}\right)^{-1} = \frac{32}{11}\approx 2.91

实际上,由于其他开销的存在,我们在这里看到的加速比约为 2.42.4(计算输出样本)。

时间统计

时间类型测量时间
每个滤波器系数13.94 ns
每个输出样本14274.91 ns
每帧3720900.00 ns

输出波形图

Part 2C 输出