【问题标题】:Why does gcc, with -O3, unnecessarily clear a local ARM NEON array?为什么带有 -O3 的 gcc 会不必要地清除本地 ARM NEON 数组?
【发布时间】:2021-11-27 23:32:27
【问题描述】:

考虑以下代码 (Compiler Explorer link),在 gcc 和 clang 下编译并使用 -O3 优化:

#include <arm_neon.h>

void bug(int8_t *out, const int8_t *in) {
    for (int i = 0; i < 2; i++) {
        int8x16x4_t x;

        x.val[0] = vld1q_s8(&in[16 * i]);
        x.val[1] = x.val[2] = x.val[3] = vshrq_n_s8(x.val[0], 7);

        vst4q_s8(&out[64 * i], x);
    }
}

注意:这是一个问题的最小可重现版本,它出现在我实际的、更复杂的代码的许多不同功能中,其中充满了执行完全不同的算术/逻辑/置换指令从上面操作。请不要批评和/或建议执行上述代码的不同方式,除非它对下面讨论的代码生成问题有影响。

clang 生成健全的代码:

bug(signed char*, signed char const*):                            // @bug(signed char*, signed char const*)
        ldr     q0, [x1]
        sshr    v1.16b, v0.16b, #7
        mov     v2.16b, v1.16b
        mov     v3.16b, v1.16b
        st4     { v0.16b, v1.16b, v2.16b, v3.16b }, [x0], #64
        ldr     q0, [x1, #16]
        sshr    v1.16b, v0.16b, #7
        mov     v2.16b, v1.16b
        mov     v3.16b, v1.16b
        st4     { v0.16b, v1.16b, v2.16b, v3.16b }, [x0]
        ret

至于gcc,它插入了很多不必要的操作,显然将最终输入到st4指令的寄存器清零:

bug(signed char*, signed char const*):
        sub     sp, sp, #128
        # mov     x9, 0
        # mov     x8, 0
        # mov     x7, 0
        # mov     x6, 0
        # mov     x5, 0
        # mov     x4, 0
        # mov     x3, 0
        # stp     x9, x8, [sp]
        # mov     x2, 0
        # stp     x7, x6, [sp, 16]
        # stp     x5, x4, [sp, 32]
        # str     x3, [sp, 48]
        ldr     q0, [x1]
        # stp     x2, x9, [sp, 56]
        # stp     x8, x7, [sp, 72]
        sshr    v4.16b, v0.16b, 7
        # str     q0, [sp]
        # ld1     {v0.16b - v3.16b}, [sp]
        # stp     x6, x5, [sp, 88]
        mov     v1.16b, v4.16b
        # stp     x4, x3, [sp, 104]
        mov     v2.16b, v4.16b
        # str     x2, [sp, 120]
        mov     v3.16b, v4.16b
        st4     {v0.16b - v3.16b}, [x0], 64
        ### ldr     q4, [x1, 16]
        ### add     x1, sp, 64
        ### str     q4, [sp, 64]
        sshr    v4.16b, v4.16b, 7
        ### ld1     {v0.16b - v3.16b}, [x1]
        mov     v1.16b, v4.16b
        mov     v2.16b, v4.16b
        mov     v3.16b, v4.16b
        st4     {v0.16b - v3.16b}, [x0]
        add     sp, sp, 128
        ret

我手动为所有可以安全取出的指令加上#前缀,不影响函数结果。

此外,以### 为前缀的指令会执行不必​​要的内存访问和返回(无论如何,### ld1 ... 之后的 mov 指令会覆盖由该 ld1 指令加载的 4 个寄存器中的 3 个),并且可以被直接加载到v0.16b 的单个负载替换——然后块中间的sshr 指令将使用v0.16b 作为其源寄存器。

据我所知,x是一个局部变量,可以单元化使用;即使不是,所有寄存器都已正确初始化,因此将它们归零以立即用值覆盖它们是没有意义的。

我倾向于认为这是一个 gcc 错误,但在报告它之前,我很好奇我是否遗漏了什么。也许有一个编译标志,一个__attribute__ 或其他我可以让gcc 生成健全代码的东西。

因此,我的问题是:我可以做些什么来生成健全的代码,或者这是我需要向 gcc 报告的错误吗?

【问题讨论】:

    标签: c gcc arm64 neon compiler-bug


    【解决方案1】:

    简短回答:欢迎来到 GCC。在使用它时不要费心优化任何东西。 Clang 也好不到哪里去。

    秘诀:将 ARM 和 ARM64 组件添加到 Visual Studio,您会惊讶于它的运行效果。但问题是,它生成的是 COFF 二进制文件,而不是 ELF,而且我找不到转换器。

    您可以使用 Ida Pro 或 dumpbin 并生成一个反汇编文件并查看它。喜欢:

    ; void __fastcall bug(char *out, const char *in)
                     EXPORT bug
    bug
                     MOV             W10, #0
                     MOV             W9, #0
    
    $LL4                                    ; CODE XREF: bug+30↓j
                     ADD             X8, X1, W9,SXTW
                     ADD             W9, W9, #0x10
                     CMP             W9, #0x20 ; ' '
                     LD1             {V0.16B}, [X8]
                     ADD             X8, X0, W10,SXTW
                     ADD             W10, W10, #0x40 ; '@'
                     SSHR            V1.16B, V0.16B, #7
                     MOV             V2.16B, V1.16B
                     MOV             V3.16B, V1.16B
                     ST4             {V0.16B-V3.16B}, [X8]
                     B.LT            $LL4
                     RET
    ; End of function bug
    

    您可以将反汇编复制粘贴到 GCC 汇编文件中。

    也不要费心报告“错误”。如果他们在听,GCC 一开始就不会这么糟糕。

    【讨论】:

    • 澄清一下 - 第一段是针对 ARM + Neon 的受众,而不是在所有支持的字段中说明关于 GCC / Clang 的一般假设,对吗?
    • @Fureeish 这不是一个普遍的假设,而是分享数十年的经验。
    • 关于ARM + Neon,还是声称它适用于更广泛的领域?我想说的是,如果要针对现代 x86-64 目标,我完全不同意这种说法。也有长期经验的支持(不仅是我的)。
    • @Fureeish ARM,尤其是 NEON。我不会争论 x68,因为幸运的是我不必处理它。 Neon intrinsic 已经存在了十多年,而 GCC 仍然无法使用。这是事实。人们应该意识到这一点。 PS:我在这里没有看到任何x86 标签。
    【解决方案2】:

    在相当当前的 gcc 开发版本上的代码生成似乎得到了极大的改进,至少在这种情况下是这样。

    安装gcc-snapshot包(日期为20210918)后,gcc生成如下代码:

    bug:
            ldr     q5, [x1]
            sshr    v4.16b, v5.16b, 7
            mov     v0.16b, v5.16b
            mov     v1.16b, v4.16b
            mov     v2.16b, v4.16b
            mov     v3.16b, v4.16b
            st4     {v0.16b - v3.16b}, [x0], 64
            ldr     q4, [x1, 16]
            mov     v0.16b, v4.16b
            sshr    v4.16b, v4.16b, 7
            mov     v1.16b, v4.16b
            mov     v2.16b, v4.16b
            mov     v3.16b, v4.16b
            st4     {v0.16b - v3.16b}, [x0]
            ret
    

    还不理想——通过更改ldrsshr 的目标寄存器,每次迭代至少可以删除两条mov 指令,但比以前要好得多。

    【讨论】:

      猜你喜欢
      • 2021-04-17
      • 1970-01-01
      • 2017-08-25
      • 1970-01-01
      • 2022-12-06
      • 1970-01-01
      • 2011-08-09
      • 1970-01-01
      • 2023-03-14
      相关资源
      最近更新 更多