NEON ASM 代码运行比 C 代码慢?

4
我正在尝试在iPhone ARM使用NEON实现高斯-牛顿优化来解决一个特定的问题。下面第一个函数是我的原始C函数。第二个是我编写的NEON汇编代码。我分别运行了它们10万次,NEON版本比C版本慢7-8倍。我认为加载(vld1.32)是最耗时间的部分。我试着删除一些指令进行了实验。
有没有人对这个问题有什么见解?谢谢!
template<class T>
inline void GaussNewtonOperationJtr8x8(T Jtr[8], const T J[8], T residual)
{
    Jtr[0] -= J[0]*residual;
    Jtr[1] -= J[1]*residual;
    Jtr[2] -= J[2]*residual;
    Jtr[3] -= J[3]*residual;
    Jtr[4] -= J[4]*residual;
    Jtr[5] -= J[5]*residual;
    Jtr[6] -= J[6]*residual;
    Jtr[7] -= J[7]*residual;    
}

inline void GaussNewtonOperationJtr8x8_NEON(NFloat Jtr[8], const NFloat J[8], NFloat residual)
{
    __asm__ volatile (
                      // load Jtr into registers
                      "vld1.32   {d0-d3}, [%0]\n\t"
                      // load J into registers
                      "vld1.32   {d4-d7}, [%1]\n\t"
                      // load residual in register
                      "vmov.f32  s16, %2\n\t"
                      // Jtr -= J*residual
                      "vmls.f32  q0, q2, d8[0]\n\t"
                      "vmls.f32  q1, q3, d8[0]\n\t"
                      // store result
                      "vst1.32   {d0-d3}, [%0]\n\t"
                      // output
                      :
                      // input
                      : "r"(Jtr), "r"(J), "r"(residual)
                      // registers
                      : "d0", "d1", "d2", "d3", "d4", "d5", "d6", "d7", "d8", "d9", "d10", "d11", "d12", "d13", "d14"
                      );
}
4个回答

6
  1. 不要使用d8-d15。在使用之前必须将它们保留到堆栈中,并在使用后恢复。编译器会放置执行此操作的指令,浪费宝贵的周期。
  2. 在使用Jtr之前,先加载J。Jtr期望在比J更晚的流水线阶段出现。
  3. 使用VLDMIA/VSTMIA而不是VLD/VST。VLDMIA/VSTMIA更快,具有管道方面的优势。
  4. 使用矢量-矢量乘法而不是矢量-标量乘法。
  5. 如果您创建一个循环版本,请在开头放置pld并展开循环,以便每次迭代从每个指针读取64字节。

除了我上面提到的那些问题 - 这是新手在NEON中经常遇到的 - 您的方法非常好。您找到了最合适的vmls指令。

干得好。

}

__asm__ volatile (
    // load residual in register
    "vdup.32  q12, %2\n\t"
    // load J into registers
    "vldmia   %1, {q10-q11}\n\t"
     // load Jtr into registers
    "vldmia   %0, {q8-q9}\n\t"
    // Jtr -= J*residual
    "vmls.f32  q8, q10, q12\n\t"
    "vmls.f32  q9, q11, q12\n\t"
    // store result
    "vstmia   %0, {q8-q9}\n\t"
    // output
    :
    // input
    : "r"(Jtr), "r"(J), "r"(residual)
    // registers
    : "q8", "q9", "q10", "q11", "q12"
);

3

编译器本身会优化由C代码生成的汇编指令。它并不是将一个代码翻译为另一个代码。

你要做的是比编译器做出更好的优化(哦,天啊)。你知道编译器为上述C代码生成的汇编代码是什么吗?如果你想让自己的汇编代码更好,那么你应该知道。

编辑:

这个主题有一个很好的讨论: 为什么ARM NEON不比纯C++更快?


我没有看到GCC编译器生成 NEON 代码,所以我正在尝试自己生成 ASM NEON 代码并将其与 C 代码进行比较。 - paul
我仔细阅读了这个链接。那么我猜我的例子使用NEON不会表现良好?我移动了指令以消除依赖关系,但并没有任何改善。 - paul
你的C代码和你编写的汇编代码之间单次执行(不是100,000次)的时间差是多少毫秒? - karlphillip
我还没有找到在iPhone上能够测试单次迭代的高分辨率计时器。 - paul
我做了:https://dev59.com/5HA65IYBdhLWcg3w9DqF,有两种方法可以实现它。 - karlphillip

3

你正在切换NEON和VFP指令。在Cortex-A8和A9上这样做会有惩罚。去掉那个VFP vmov.f32指令,并确保除非有长时间运行的VFP代码来证明管道上下文切换的必要性,否则不要将此代码内联到使用VFP代码的地方。


谢谢。有没有其他方法将单精度数字放入NEON寄存器中?我需要将“残差”参数放入寄存器中。 - paul
将其作为两个浮点数数组的第一个,并将其加载到D寄存器中。一般来说,双精度和四倍精度浮点运算是NEON,单精度浮点运算是VFP。 - ohmantics

1

你的C++版本是否实际使用了浮点数?我无法确定,因为你只给出了模板,没有显示你使用了哪个实例化。对于这段代码来说,NEON比Cortex-A8上的VFP慢得多非常奇怪,但对于u32s,我可以看到可能会这样。

我不知道ABI是什么,但传递剩余部分的方式可能会有一些开销(也就是编译器将其放入%2寄存器中所做的操作)。尝试使用指针,然后在单个元素上使用vld1 - 你可以以这种方式在NEON中加载一个浮点数。

如果你使用16字节对齐的加载和存储,数组的性能会更好,但你可能需要玩一些游戏才能使输入以这种方式工作。不幸的是,你永远无法获得真正出色的性能,因为你没有避免大多数vmls指令的延迟,这是很长的(由于将NEON乘法和加法管道端到端链接)。由于依赖指令是存储指令,需要尽早在NEON管道中输入它。理想情况下,你将能够同时执行几个这样的操作,并且可以交错多个实例 - 尽可能多地装入寄存器。


是的,C++版本正在使用浮点数。 - paul

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接