用内在函数初始化 __m128i 常量的最快方法?

Fastest way to initialize a __m128i constant with intrinsics?

目前,我有一个__m128i变量,我们称它为X。我想用一个常量 128 位值对它进行异或并将该值保存回 X。因此,本质上 X ^= C 对于某个常数 C.

目前,我正在做一些事情:

X = _mm_xor_si128(X, _mm_set_epi64x(C_a, C_b))

它从 C 的两个 64 位部分构建一个 __m128i 用于 xor。

我的问题是,这似乎不是为异或初始化 __m128i 常量的最有效方法。尝试从对齐的数组中加载一些会更好吗?或者其他一些方法?

我目前正在 Visual Studio 中使用 MSVC。

这个答案纯粹是关于常量C的情况。如果你有非常量输入,那么它们来自哪里很重要(内存、寄存器、你可能首先在向量寄存器中做的最近计算?)以及你正在做什么来处理结果向量。将单独的标量变量混入/移出 SIMD 向量有点糟糕,需要在 ALU 端口瓶颈与延迟和 store/reload 的吞吐量之间进行权衡(以及标量 -> 向量的存储转发停顿)。 Store/reload 在 asm 中很好地获取大量小元素 out SIMD 向量,但是当你想要它们时。


对于常量 C_aC_b,即使 MSVC 在通过 _mm_set 进行常量传播方面也做得很好。所以写一个像

这样的特定于实现的初始化程序没有任何优势

请记住,性能的真正决定因素是您可以诱使编译器生成的程序集,而不是您使用哪些内部函数来执行此操作。

#include <immintrin.h>

__m128i xor_const(__m128i v) {
    return _mm_xor_si128(v, _mm_set_epi64x(0x789abc, 0x123456));
}

使用 x64 MSVC -O2 Gv 编译 (on Godbolt)(使用 vectorcall 以便我们可以看到当向量已经在寄存器中时它做了什么,比如当这个内联时),我们得到这个相当愚蠢的 asm,希望内联后在更大的函数中不会这么糟糕:

;; MSVC 19.10
;; this is in the .rdata section; godbolt just filters directives that aren't interesting
;; "everyone knows" that compilers put data in the right sections
__xmm@0000000000789abc0000000000123456 DB 'V4', 012H, 00H, 00H, 00H, 00H, 00H
        DB      0bcH, 09aH, 'x', 00H, 00H, 00H, 00H, 00H

xor_const@@16 PROC                                  ; COMDAT
        movdqa  xmm1, XMMWORD PTR __xmm@0000000000789abc0000000000123456
        pxor    xmm1, xmm0
        movdqa  xmm0, xmm1
        ret     0
xor_const@@16 ENDP

我们可以看到 _mm_set 内部编译为静态存储中的 16 字节常量,如我们所愿。没有使用 pxor xmm0, xmm1 是令人惊讶的,但是 MSVC is well known for asm that's often not quite as good 与 GCC and/or clang 相比。同样,作为大型函数的一部分,当它可以选择寄存器时,我们可能没有额外的 movdqa。如果 xor 在一个循环中,那么在循环外加载一次就是我们想要的。这不是最新的 MSVC 版本; Godbolt 只为 C++ 安装了最新的 MSVC 版本,而不是 C,但你标记了这个 C。


相比之下,GCC9.2 -O3 编译为在所有 CPU 上都有效的预期内存源 PXOR。

xor_const:
        pxor    xmm0, XMMWORD PTR .LC0[rip]
        ret

.section .rodata    # Godbolt strips out stuff like section directive; re-added manually
.LC0:
        .quad   1193046
        .quad   7903932

您可能会让编译器发出相同的 asm,其中包含一个包含常量的静态 alignas(16) 数组,然后 _mm_load_si128() 从中生成。但何必呢?

避免的一件事是编写static const __m128i C = _mm_set...——编译器对此非常愚蠢,不会折叠_mm_set 变成 __m128i 的静态常量初始值设定项。 C 编译器将拒绝编译非常量静态初始值设定项。 C++ 编译器将保留一些 BSS space 和 运行 一个类似构造函数的函数,用于从只读常量复制到 BSS space,因为 _mm_set 没有完全优化在那种情况下离开。