c++ - GCC w/inline assembly & -Ofast 为内存操作数生成额外代码

标签 c++ gcc x86-64 compiler-optimization inline-assembly

我正在将索引的地址输入到扩展的内联汇编操作中的表中,但是 GCC 在不需要时生成额外的 lea 指令,即使在使用 -Ofast 时也是如此-fomit-frame-pointer-Os -f...。 GCC 使用 RIP 相关地址。

我正在创建一个函数,用于将两个连续位转换为由两部分组成的 XMM 掩码(每位 1 个四字掩码)。为此,我使用 _mm_cvtepi8_epi64(内部 vpmovsxbq)和一个 8 字节表中的内存操作数,其中位作为索引。

当我使用内部函数时,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 相对寻址模式是 RIP + rel32RIP + 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 可执行文件(或一般的 PIC 代码,如库)中,您需要一个 RIP 相关的 LEA 来设置索引静态数组。或者静态地址不适合 32 位和/或不是链接时间常量的任何其他情况。


内部函数

是的,内在函数使得从窄源表达 pmovzx/sx 加载变得非常不方便,因为缺少内在函数的指针源版本。

*((__m128i*) &MASK_TABLE[mask] 不安全:如果禁用优化,您很可能会得到一个 movdqa 16 字节加载,但地址将错位。只有当编译器将负载折叠到 pmovzxbq 的内存操作数时才是安全的,它有一个 2 字节的内存操作数,因此不需要对齐。

事实上,当前的 GCC 确实使用 movdqa 16 字节加载编译您的代码,如 movdqa xmm0, XMMWORD PTR [rax+rdi*2] 在 reg-reg pmovzx 之前。这显然是一个错过的优化。 :( clang/LLVM(MacOS 安装为 gcc)确实将负载折叠到 pmovzx 中。

安全的方法是 _mm_cvtepi8_epi64( _mm_cvtsi32_si128(MASK_TABLE[mask]) ) 之类的,然后希望编译器将零扩展从 2 字节优化到 4 字节并折叠 movd 启用优化时加载。或者尝试使用 _mm_loadu_si32 进行 32 位加载,即使您真的想要 16 位。但是上次我尝试时,编译器无法将 64 位加载内在函数折叠到 pmovzxbw< 的内存操作数中 例如。 GCC 和 clang 仍然失败,但 ICC19 成功了。 https://godbolt.org/z/IdgoKV

我之前写过:


你的整数 -> vector 策略

您对 pmovsx 的选择似乎很奇怪。您不需要符号扩展,所以我会选择 pmovzx (_mm_cvt_epu8_epi64)。不过,它实际上并没有在任何 CPU 上更高效。

查找表在这里工作,只需要少量静态数据。如果你的 mask 范围更大,你可能想看看 is there an inverse instruction to the movemask instruction in intel avx2?对于广播 + AND +(移位或比较)等替代策略。

如果您经常这样做,使用 4x 16 字节 vector 常量的整个缓存行可能是最好的,这样您就不需要 pmovzx 指令,只需索引到 的对齐表中>xmm2__m128i vector ,它们可以是任何其他 SSE 指令的内存源。使用 alignas(64) 获取同一缓存行中的所有常量。

如果您的目标是 Intel CPU,您还可以考虑 (intrinsics for) pdep + movd xmm0, eax + pmovzxbq reg-reg与 BMI2。 (不过,pdep 在 AMD 上运行缓慢)。

关于c++ - GCC w/inline assembly & -Ofast 为内存操作数生成额外代码,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/56985115/

相关文章:

c - gcc 检查文件是否是主文件 (#if __BASE_FILE__ == __FILE__)

c++ - 使用不同类型的 Compare 实例构造 priority_queue 实例

feenableexcept 会损害程序性能吗?

linux - NASM x86_64 在编写命令行参数时遇到问题,在 rax 中返回 -14

c++ - 合并两个QIcon

c++ - 原始树的子树

c++ - 在Visual C++中使用MessageBoxW输出多个相加的字符串

c++ - 为什么有不同的输入值?

c - 为内联汇编创建常量池的正确方法是什么?

c - 扩展装配中 cmpxchg16b 的不可能约束