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

Posted

技术标签:

【中文标题】用内在函数初始化 __m128i 常量的最快方法?【英文标题】:Fastest way to initialize a __m128i constant with intrinsics? 【发布时间】:2020-03-20 00:35:11 【问题描述】:

目前,我有一个 __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。

【问题讨论】:

【参考方案1】:

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


对于常量C_aC_b,即使是MSVC 在通过_mm_set 进行常量传播方面也做得很好。所以编写像SSE Error - Using m128i_i32 to define fields of a __m128i variable这样的特定于实现的初始化程序没有任何优势

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

#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 以便我们可以看到当向量已经在寄存器中时它会做什么,例如内联时),我们得到这个相当愚蠢的汇编,希望在内联后在更大的函数中不会那么糟糕:

;; 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 令人惊讶,但与 GCC 和/或 clang 相比,MSVC is well known for asm that's often not quite as good。同样,作为一个大函数的一部分,当它可以选择寄存器时,我们可能没有额外的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 折叠成@987654338 的静态常量初始化器@。 C 编译器将拒绝编译非常量静态初始化程序。 C++ 编译器将保留一些 BSS 空间并运行一个类似构造函数的函数以从只读常量复制到该 BSS 空间,因为在这种情况下 _mm_set 不会完全优化。

【讨论】:

谢谢你——这里有很多很棒的信息,我现在还在更详细地讨论它。看起来,在当前的实现中,我从代码段中的常量(cs:xmmword...)中得到了一个movdqa。我将尝试您使用对齐数组的建议来让它只做 pxor。但你也说我不应该打扰——我为什么不应该呢?似乎是更高效的解决方案。 @user2059300:你实际上是在组装我从 Godbolt 编译器资源管理器复制/粘贴的 asm 的 sn-p 吗?它过滤掉了section .rdata 之类的指令,因此您只能看到基本部分。如果您想实际组装它,请使用 Godbolt 上完整源代码 + asm 的链接,并使用下拉列表取消过滤。 但你真的希望编译器内联这个 C 函数。 这样做的目的是了解编译器一般会做什么,而不是创建它具有的 asm 函数实际上call!!那太可怕了。 @user2059300:但你也说我不应该打扰——为什么不呢?因为就像我在答案中解释的那样,编译器会(或应该)如果您以这种方式编写源代码,则会生成相同的 asm。我想你错过了我回答的全部要点。由于_mm_set 方式更易于阅读,请改用它。编写源代码的不同方式有时可能会让编译器避免错过优化,但是当内联到实际代码中时,您无法根据具体情况知道哪种方式更好。

以上是关于用内在函数初始化 __m128i 常量的最快方法?的主要内容,如果未能解决你的问题,请参考以下文章

_mm256_loadu2_m128i 内在函数在 g++ 下不可用?

使用 sse 内在函数时如何打破循环?

在 __m128i 向量上水平检查零?

通过引用内联函数传递 __m128i 对象会导致这些对象移动到堆栈吗?

SSE 将整数加载到 __m128

如何在 MASM 中声明 __m128i 常量?