【问题标题】:NEON ASM code running much slower than C code?NEON ASM 代码运行速度比 C 代码慢得多?
【发布时间】:2011-08-27 10:58:54
【问题描述】:

我正在尝试使用 NEON 针对 iPhone ARM 上的特定问题实施高斯牛顿优化。下面的第一个函数是我原来的 C 函数。第二个是我写的NEON asm代码。我每一个都跑了 100,000 次,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"
                      );
}

【问题讨论】:

    标签: assembly arm neon


    【解决方案1】:

    编译器本身会优化 C 代码生成的程序集。它只是不会将一种代码转换为另一种代码。

    您正在尝试做的是比编译器进行更好的优化(哦哦)。你至少知道编译器为上面的 C 代码生成的汇编代码是什么吗?好吧,如果你想让你的汇编代码更好,你应该这样做。

    编辑:

    这个帖子对这类东西进行了很好的讨论: Why ARM NEON not faster than plain C++?

    【讨论】:

    • 我没有看到GCC编译器生成NEON代码。所以我正在尝试自己生成 ASM NEON 代码并与 C 代码进行比较。
    • 我仔细阅读了这个链接。所以我想我的例子使用 NEON 表现不佳?我移动了说明以消除依赖关系,但没有任何改进。
    • 您的 C 代码与您提出的程序集之间的单次执行(不是 100.000 次)之间的时间差(以毫秒为单位)是多少?
    • 我还没有找到一个分辨率足够高的计时器,可以在 iPhone 上只测试一次迭代。
    • 我做了:stackoverflow.com/q/3540234/176769,有两种方法可以做到这一点。
    【解决方案2】:

    您正在 NEON 和 VFP 指令之间切换。在 Cortex-A8 和 A9 上这样做都会受到惩罚。摆脱 VFP vmov.f32 指令,并确保此代码不会内联到使用 VFP 代码的地方,除非有长时间运行此类代码来证明流水线上下文切换是合理的。

    【讨论】:

    • 谢谢。是否有另一种方法可以将单精度数字输入 NEON 寄存器?我需要将“剩余”参数放入寄存器中。
    • 使其成为两个浮点数数组中的第一个,并将其加载到 D 寄存器中。一般来说,双浮点和四浮点运算是NEON,单浮点运算是VFP。
    【解决方案3】:

    您的 C++ 版本实际上是否使用浮点数?我不知道,因为您只提供了模板并且没有显示您使用的实例化。很奇怪,对于这段代码,NEON 在 Cortex-A8 上比 VFP 慢得多,但对于 u32s,我可以看到它可能会以这种方式工作。

    我不知道 ABI 是什么,但是对于如何传递残差可能会有一些开销(即编译器正在做什么以将其放入 %2 寄存器)。尝试改用指针并在单个元素上使用 vld1 - 您可以通过这种方式在 NEON 中仅加载一个浮点数。

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

    【讨论】:

    • 是的,C++ 版本使用浮点数。
    【解决方案4】:
    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"
    );
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2016-10-23
      • 1970-01-01
      • 2019-04-23
      • 1970-01-01
      • 2020-08-29
      • 2017-01-19
      • 1970-01-01
      • 2021-08-28
      相关资源
      最近更新 更多