为什么带有 -O3 的 gcc 会不必要地清除本地 ARM NEON 阵列?
Why does gcc, with -O3, unnecessarily clear a local ARM NEON array?
考虑以下代码 (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);
}
}
注意:这是一个问题的最小可重现版本,它出现在我实际的、更复杂的代码的许多不同功能中,充满了 arithmetic/logical/permutation执行与上面完全不同的操作的指令。请不要批评 and/or 建议以不同的方式执行上面的代码,除非它对下面讨论的代码生成问题有影响。
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
指令会覆盖由加载的 4 个寄存器中的 3 个该 ld1
指令),并且可以被直接加载到 v0.16b
的单个负载替换——然后块中间的 sshr
指令将使用 v0.16b
作为它的源寄存器。
据我所知,x
是局部变量,可以单元化使用;即使不是,所有寄存器都已正确初始化,因此将它们清零只是为了立即用值覆盖它们是没有意义的。
我倾向于认为这是一个 gcc 错误,但在报告之前,我想知道我是否遗漏了什么。也许有一个编译标志,一个 __attribute__
或其他我可以使 gcc
生成合理代码的东西。
因此,我的问题是:我能做些什么来生成合理的代码,还是我需要向 gcc 报告这个错误?
简短回答:欢迎来到 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 一开始就不会这么糟糕。
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
还不理想 -- 通过更改 ldr
和 sshr
的目标寄存器,每次迭代至少可以删除两条 mov
指令,但比以前好得多。
考虑以下代码 (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);
}
}
注意:这是一个问题的最小可重现版本,它出现在我实际的、更复杂的代码的许多不同功能中,充满了 arithmetic/logical/permutation执行与上面完全不同的操作的指令。请不要批评 and/or 建议以不同的方式执行上面的代码,除非它对下面讨论的代码生成问题有影响。
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
指令会覆盖由加载的 4 个寄存器中的 3 个该 ld1
指令),并且可以被直接加载到 v0.16b
的单个负载替换——然后块中间的 sshr
指令将使用 v0.16b
作为它的源寄存器。
据我所知,x
是局部变量,可以单元化使用;即使不是,所有寄存器都已正确初始化,因此将它们清零只是为了立即用值覆盖它们是没有意义的。
我倾向于认为这是一个 gcc 错误,但在报告之前,我想知道我是否遗漏了什么。也许有一个编译标志,一个 __attribute__
或其他我可以使 gcc
生成合理代码的东西。
因此,我的问题是:我能做些什么来生成合理的代码,还是我需要向 gcc 报告这个错误?
简短回答:欢迎来到 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 一开始就不会这么糟糕。
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
还不理想 -- 通过更改 ldr
和 sshr
的目标寄存器,每次迭代至少可以删除两条 mov
指令,但比以前好得多。