GCC 带内联汇编 & -Ofast 为内存操作数生成额外代码
GCC w/ inline assembly & -Ofast generating extra code for memory operand
我正在将索引的地址输入 table 到扩展的内联汇编操作中,但是 GCC 在不需要时生成额外的 lea
指令,即使使用 -Ofast -fomit-frame-pointer
或 -Os -f...
。 GCC 正在使用 RIP 相关地址。
我正在创建一个函数,用于将两个连续位转换为两部分 XMM 掩码(每位 1 个四字掩码)。为此,我使用 _mm_cvtepi8_epi64
(内部 vpmovsxbq
)和来自 8 字节 table 的内存操作数,其中位作为索引。
当我使用内在函数时,GCC 生成的代码与使用扩展内联汇编的代码完全相同。
我可以直接将内存操作嵌入到 ASM 模板中,但这将始终强制使用 RIP 相对寻址(而且我不喜欢强迫自己采用变通方法)。
typedef uint64_t xmm2q __attribute__ ((vector_size (16)));
// Used for converting 2 consecutive bits (as index) into a 2-elem XMM mask (pmovsxbq)
static const uint16_t MASK_TABLE[4] = { 0x0000, 0x0080, 0x8000, 0x8080 };
xmm2q mask2b(uint64_t mask) {
assert(mask < 4);
#ifdef USE_ASM
xmm2q result;
asm("vpmovsxbq %1, %0" : "=x" (result) : "m" (MASK_TABLE[mask]));
return result;
#else
// bad cast (UB?), but input should be `uint16_t*` anyways
return (xmm2q) _mm_cvtepi8_epi64(*((__m128i*) &MASK_TABLE[mask]));
#endif
}
带 -S
的输出组件(带 USE_ASM
和不带):
__Z6mask2by: ## @_Z6mask2by
.cfi_startproc
## %bb.0:
leaq __ZL10MASK_TABLE(%rip), %rax
vpmovsxbq (%rax,%rdi,2), %xmm0
retq
.cfi_endproc
我所期待的(我删除了所有多余的东西):
__Z6mask2by:
vpmovsxbq __ZL10MASK_TABLE(%rip,%rdi,2), %xmm0
retq
唯一的 RIP-relative 寻址模式是 RIP + rel32
。 RIP + reg 不可用。
(在机器代码中,32 位代码曾经有 2 种冗余编码方式 [disp32]
。x86-64 使用较短的(无 SIB)形式作为 RIP 相对,较长的 SIB 形式作为 [sign_extended_disp32]
).
如果用 -fno-pie -no-pie
为 Linux 编译,GCC 将能够访问具有 32 位绝对地址的静态数据,因此它可以使用像 __ZL10MASK_TABLE(,%rdi,2)
这样的模式。这对于 MacOS 是不可能的,它的基地址总是在 2^32 以上; x86-64 MacOS 完全不支持 32 位绝对寻址。
在 PIE executable(或一般的 PIC 代码,如库)中,您需要一个 RIP-relative LEA 来设置索引静态数组。或者静态地址不适合 32 位的任何其他情况 and/or 不是 link-time 常量。
内在函数
是的,内在函数使得表达来自狭窄源的 pmovzx/sx
负载非常不方便,因为缺少 pointer-source 版本的内在函数。
*((__m128i*) &MASK_TABLE[mask]
不安全:如果禁用优化,您很可能会得到 movdqa
16 字节的加载,但地址会错位。仅当编译器将负载折叠到 pmovzxbq
的内存操作数时才是安全的,该内存操作数具有 2 字节内存操作数,因此不需要对齐。
事实上,当前的 GCC 确实 使用 movdqa
16 字节加载来编译您的代码,例如 movdqa xmm0, XMMWORD PTR [rax+rdi*2]
在 reg-reg [=21] 之前=].这显然是一个错过的优化。 :( clang/LLVM(MacOS 安装为 gcc
)确实将负载折叠到 pmovzx
。
安全的方法是 _mm_cvtepi8_epi64( _mm_cvtsi32_si128(MASK_TABLE[mask]) )
之类的,然后希望编译器将 zero-extend 从 2 字节优化到 4 字节,并在启用优化时将 movd
折叠到负载中.或者也许尝试 _mm_loadu_si32
进行 32 位加载,即使您真的想要 16 位。但是上次我尝试时,编译器在将 64 位加载内在函数折叠到 pmovzxbw
的内存操作数时表现不佳。 GCC 和 clang 仍然失败,但 ICC19 成功了。 https://godbolt.org/z/IdgoKV
我之前写过:
- How to merge a scalar into a vector without the compiler wasting an instruction zeroing upper elements? Design limitation in Intel's intrinsics?
你的整数 -> 向量策略
您选择 pmovsx
似乎很奇怪。您不需要 sign-extension,所以我会选择 pmovzx
(_mm_cvt_epu8_epi64
)。不过,它实际上并没有在任何 CPU 上更高效。
查找 table 在这里工作,只需要少量静态数据。如果你的面罩范围更大,你可能想看看
is there an inverse instruction to the movemask instruction in intel avx2? 替代策略,例如广播 + AND +(移位或比较)。
如果您经常这样做,最好使用 4x 16 字节向量常量的整个缓存行,这样您就不需要 pmovzx
指令,只需索引对齐的 table xmm2
或 __m128i
向量,可以作为任何其他 SSE 指令的内存源。使用alignas(64)
获取同一缓存行中的所有常量。
如果您的目标是具有 BMI2 的英特尔 CPU,您还可以考虑(内在函数)pdep
+ movd xmm0, eax
+ pmovzxbq
reg-reg。 (不过,pdep
在 AMD 上运行缓慢)。
我正在将索引的地址输入 table 到扩展的内联汇编操作中,但是 GCC 在不需要时生成额外的 lea
指令,即使使用 -Ofast -fomit-frame-pointer
或 -Os -f...
。 GCC 正在使用 RIP 相关地址。
我正在创建一个函数,用于将两个连续位转换为两部分 XMM 掩码(每位 1 个四字掩码)。为此,我使用 _mm_cvtepi8_epi64
(内部 vpmovsxbq
)和来自 8 字节 table 的内存操作数,其中位作为索引。
当我使用内在函数时,GCC 生成的代码与使用扩展内联汇编的代码完全相同。
我可以直接将内存操作嵌入到 ASM 模板中,但这将始终强制使用 RIP 相对寻址(而且我不喜欢强迫自己采用变通方法)。
typedef uint64_t xmm2q __attribute__ ((vector_size (16)));
// Used for converting 2 consecutive bits (as index) into a 2-elem XMM mask (pmovsxbq)
static const uint16_t MASK_TABLE[4] = { 0x0000, 0x0080, 0x8000, 0x8080 };
xmm2q mask2b(uint64_t mask) {
assert(mask < 4);
#ifdef USE_ASM
xmm2q result;
asm("vpmovsxbq %1, %0" : "=x" (result) : "m" (MASK_TABLE[mask]));
return result;
#else
// bad cast (UB?), but input should be `uint16_t*` anyways
return (xmm2q) _mm_cvtepi8_epi64(*((__m128i*) &MASK_TABLE[mask]));
#endif
}
带 -S
的输出组件(带 USE_ASM
和不带):
__Z6mask2by: ## @_Z6mask2by
.cfi_startproc
## %bb.0:
leaq __ZL10MASK_TABLE(%rip), %rax
vpmovsxbq (%rax,%rdi,2), %xmm0
retq
.cfi_endproc
我所期待的(我删除了所有多余的东西):
__Z6mask2by:
vpmovsxbq __ZL10MASK_TABLE(%rip,%rdi,2), %xmm0
retq
唯一的 RIP-relative 寻址模式是 RIP + rel32
。 RIP + reg 不可用。
(在机器代码中,32 位代码曾经有 2 种冗余编码方式 [disp32]
。x86-64 使用较短的(无 SIB)形式作为 RIP 相对,较长的 SIB 形式作为 [sign_extended_disp32]
).
如果用 -fno-pie -no-pie
为 Linux 编译,GCC 将能够访问具有 32 位绝对地址的静态数据,因此它可以使用像 __ZL10MASK_TABLE(,%rdi,2)
这样的模式。这对于 MacOS 是不可能的,它的基地址总是在 2^32 以上; x86-64 MacOS 完全不支持 32 位绝对寻址。
在 PIE executable(或一般的 PIC 代码,如库)中,您需要一个 RIP-relative LEA 来设置索引静态数组。或者静态地址不适合 32 位的任何其他情况 and/or 不是 link-time 常量。
内在函数
是的,内在函数使得表达来自狭窄源的 pmovzx/sx
负载非常不方便,因为缺少 pointer-source 版本的内在函数。
*((__m128i*) &MASK_TABLE[mask]
不安全:如果禁用优化,您很可能会得到 movdqa
16 字节的加载,但地址会错位。仅当编译器将负载折叠到 pmovzxbq
的内存操作数时才是安全的,该内存操作数具有 2 字节内存操作数,因此不需要对齐。
事实上,当前的 GCC 确实 使用 movdqa
16 字节加载来编译您的代码,例如 movdqa xmm0, XMMWORD PTR [rax+rdi*2]
在 reg-reg [=21] 之前=].这显然是一个错过的优化。 :( clang/LLVM(MacOS 安装为 gcc
)确实将负载折叠到 pmovzx
。
安全的方法是 _mm_cvtepi8_epi64( _mm_cvtsi32_si128(MASK_TABLE[mask]) )
之类的,然后希望编译器将 zero-extend 从 2 字节优化到 4 字节,并在启用优化时将 movd
折叠到负载中.或者也许尝试 _mm_loadu_si32
进行 32 位加载,即使您真的想要 16 位。但是上次我尝试时,编译器在将 64 位加载内在函数折叠到 pmovzxbw
的内存操作数时表现不佳。 GCC 和 clang 仍然失败,但 ICC19 成功了。 https://godbolt.org/z/IdgoKV
我之前写过:
- How to merge a scalar into a vector without the compiler wasting an instruction zeroing upper elements? Design limitation in Intel's intrinsics?
你的整数 -> 向量策略
您选择 pmovsx
似乎很奇怪。您不需要 sign-extension,所以我会选择 pmovzx
(_mm_cvt_epu8_epi64
)。不过,它实际上并没有在任何 CPU 上更高效。
查找 table 在这里工作,只需要少量静态数据。如果你的面罩范围更大,你可能想看看 is there an inverse instruction to the movemask instruction in intel avx2? 替代策略,例如广播 + AND +(移位或比较)。
如果您经常这样做,最好使用 4x 16 字节向量常量的整个缓存行,这样您就不需要 pmovzx
指令,只需索引对齐的 table xmm2
或 __m128i
向量,可以作为任何其他 SSE 指令的内存源。使用alignas(64)
获取同一缓存行中的所有常量。
如果您的目标是具有 BMI2 的英特尔 CPU,您还可以考虑(内在函数)pdep
+ movd xmm0, eax
+ pmovzxbq
reg-reg。 (不过,pdep
在 AMD 上运行缓慢)。