SAD 16*4 的 Arm-neon 优化版本未提供预期增益
Posted
技术标签:
【中文标题】SAD 16*4 的 Arm-neon 优化版本未提供预期增益【英文标题】:Arm-neon optimized version of SAD 16*4 not giving expected gain 【发布时间】:2014-09-30 06:14:28 【问题描述】:我写了一个 16*4 SAD 函数和它的 arm-neon 优化版本。 arm-neon 版本是用内联汇编编写的。 我的问题是我只得到了 2 倍优化(启用 O3),而理想情况下我应该得到至少 6 倍的优化。 谁能解释一下正在发生的事情的内部情况?
static unsigned int f_sad_16x4 ( const unsigned char* a, const unsigned char* b, const unsigned int uiStrideOrg, const unsigned int uiStrideCur )
unsigned int sad = 0;
for (int i = 0; i < 4; i++)
for (int j = 0; j < 16; j++)
sad += abs(static_cast<int>(a[i*uiStrideOrg+j]) - static_cast<int>(b[i*uiStrideCur+j]));
return sad;
static unsigned int f_sad_16x4_neon(const unsigned char* a, const unsigned char* b, const unsigned int uiStrideOrg, const unsigned int uiStrideCur )
unsigned short auiSum[8];
unsigned short* puiSum = auiSum;
__asm__ volatile( \
/* Load 4 rows of piOrg and piCur each */
"vld1.8 q0,[%[piOrg]],%[iStrideOrg] \n\t"\
"vld1.8 q4,[%[piCur]],%[iStrideCur] \n\t"\
"vld1.8 q1,[%[piOrg]],%[iStrideOrg] \n\t"\
"vabd.u8 q8, q0, q4 \n\t"\
"vld1.8 q5,[%[piCur]],%[iStrideCur] \n\t"\
"vld1.8 q2,[%[piOrg]],%[iStrideOrg] \n\t"\
"vabd.u8 q9, q1, q5 \n\t"\
"vld1.8 q6,[%[piCur]],%[iStrideCur] \n\t"\
"vld1.8 q3,[%[piOrg]],%[iStrideOrg] \n\t"\
"vabd.u8 q10, q2, q6 \n\t"\
"vld1.8 q7,[%[piCur]],%[iStrideCur] \n\t"\
"vpaddl.u8 q12, q8 \n\t"\
"vabd.u8 q11, q3, q7 \n\t"\
"vpaddl.u8 q13, q9 \n\t"\
"vpaddl.u8 q14, q10 \n\t"\
"vadd.u16 q8, q12, q13 \n\t"\
"vpaddl.u8 q15, q11 \n\t"\
"vadd.u16 q9, q14, q15 \n\t"\
"vadd.u16 q0, q8, q9 \n\t"\
"vst1.16 q0, [%[puiSum]] \n\t"\
:[piOrg] "+r" (a),
[piCur] "+r" (b),
[puiSum] "+r" (puiSum)
:[iStrideCur] "r" (uiStrideCur),
[iStrideOrg] "r" (uiStrideOrg)
:"q0","q1","q2","q3","q4","q5","q6","q7","q8","q9","q10","q11","q12","q13","q14","q15"
);
unsigned int uiSum += auiSum[0] + auiSum[1] + auiSum[2] + auiSum[3] + auiSum[4] + auiSum[5] + auiSum[6] + auiSum[7];
return uiSum;
【问题讨论】:
在函数中执行如此少量的工作可能意味着函数调用开销会交换任何优化收益 - 如果您在循环中调用此代码,则考虑 (a) 重构,以便优化的代码在循环中而不是在单独的函数中,并将任何设置/拆除的东西移出循环或(b)使用内在函数重新编写函数并使其内联 - 这样编译器就可以摆脱任何函数的前导码/后同步码代码,并重新分配指令,甚至可能在分配寄存器方面做得更好。 您是否检查了编译器从第一个函数中生成的汇编代码?也许它已经在使用霓虹灯了…… 未优化版本没有使用霓虹灯指令 这里是生成的汇编代码:onedrive.live.com/… 将近一半的代码是加载和存储——您是否期望发现内存带宽突然增加三倍? ;) 【参考方案1】:此代码性能不佳,因为编译器必须在您的内联汇编程序块中发出 20 条 NEON 指令之外的 23 条整数指令。
最简单的修复部分是这一行:
unsigned int uiSum += auiSum[0] + auiSum[1] + auiSum[2] + auiSum[3] + auiSum[4] + auiSum[5] + auiSum[6] + auiSum[7];
最后的还原步骤可以在 NEON 单元上执行。例如
VADDL.S16 q0, d0, d1 // 32 bit lanes in q0
VPADDL.S32 q0, q0 // 64 bit lanes in q0
VADD.I64 d0, d0, d1 // one 64 bit result in d0
然后您可以通过一次移动来检索结果:
VMOV %n, %Hn, d0 // retrieve 64 bit result
在上面,您需要设置 n 来对应内联 asm 输出块中结果变量的适当操作数。
另一个问题是寄存器分配不是最理想的。寄存器 d8 到 d15(q4 到 q7)必须由使用它们的任何函数保留,因此编译器会发出代码来执行此操作。您可以重写您的函数以重用寄存器并避免使用这些寄存器。
此函数将受益于使用 NEON 内在函数。这将避免担心寄存器分配的需要,并且还将使您的代码可移植到 Aarch64
【讨论】:
我不明白你说的d8到d15必须保留的部分。为什么会这样? 这是 ARM ABI 的要求。编译器假定函数调用不会更改 d8-d15 中的值,因此它将变量存储在这些寄存器中。因此,使用这些寄存器的函数必须在返回之前恢复原始值,以防止调用函数得到错误的结果。 如果我没记错的话,管道间数据传输的延迟太大,因此必须避免。从这个意义上说,最后使用 VMOV %n, %Hn, d0 指令是否明智? 如果将数据从 NEON 寄存器传输到 ARM 寄存器,无论使用何种指令序列,流水线都会产生不可避免的延迟。在某些微架构上,您可以使用VST1, <lots of independent work>, LDR
之类的序列来减少惩罚,但VST1, LDR
作为相邻对几乎肯定会比VMOV
执行得更差
@CharlesBaylis 当两个内核访问相同的内存时,同样的 12 个周期的惩罚也适用。以上是关于SAD 16*4 的 Arm-neon 优化版本未提供预期增益的主要内容,如果未能解决你的问题,请参考以下文章
OpenCV - Python 断言错误:SAD 算法 - 立体相机视差图计算