用内在函数初始化 __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_a
和C_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++ 下不可用?