更新:现在有一个__m128i _mm256_zextsi128_si256(__m128i)
内在的;见Agner Fog 的回答。下面的其余答案仅与不支持此内在函数的旧编译器相关,并且没有高效、可移植的解决方案。
不幸的是,理想的解决方案将取决于您使用的编译器,并且对于其中一些,没有理想的解决方案。
我们可以通过几种基本方式来编写它:
版本 A:
ymm = _mm256_set_m128i(_mm_setzero_si128(), _mm256_castsi256_si128(ymm));
版本 B:
ymm = _mm256_blend_epi32(_mm256_setzero_si256(),
ymm,
_MM_SHUFFLE(0, 0, 3, 3));
版本 C:
ymm = _mm256_inserti128_si256(_mm256_setzero_si256(),
_mm256_castsi256_si128(ymm),
0);
它们中的每一个都精确地完成了我们想要的操作,清除 256 位 YMM 寄存器的高 128 位,因此可以安全地使用它们中的任何一个。但哪个是最优化的?好吧,这取决于您使用的是哪个编译器...
海湾合作委员会:
版本 A:根本不支持,因为 GCC 缺少_mm256_set_m128i
内在函数。(当然可以模拟,但可以使用“B”或“C”中的一种形式来完成。)
版本 B:编译为低效代码。成语不被识别,内在函数被非常字面地翻译成机器代码指令。使用 将临时 YMM 寄存器归零VPXOR
,然后使用 将其与输入 YMM 寄存器混合VPBLENDD
。
版本 C:理想。尽管代码看起来有点吓人且效率低下,但所有支持 AVX2 代码生成的 GCC 版本都可以识别这个习语。你得到了预期的VMOVDQA xmm?, xmm?
指令,它隐式地清除了高位。
更喜欢C版!
铿锵声:
版本 A:编译为低效代码。使用 将临时 YMM 寄存器归零VPXOR
,然后使用VINSERTI128
(或浮点形式,取决于版本和选项)将其插入到临时 YMM 寄存器中。
版本 B 和 C:也编译为低效代码。临时 YMM 寄存器再次归零,但在这里,它与输入 YMM 寄存器混合使用VPBLENDD
。
没有什么理想的!
国际商会:
版本 A:理想。产生预期的VMOVDQA xmm?, xmm?
指令。
版本 B:编译为低效代码。将临时 YMM 寄存器归零,然后将零与输入 YMM 寄存器 ( VPBLENDD
) 混合。
版本 C:也编译为低效代码。将临时 YMM 寄存器归零,然后用于VINSERTI128
将零插入临时 YMM 寄存器。
更喜欢A版!
MSVC:
版本 A 和 C:编译为低效代码。将临时 YMM 寄存器清零,然后使用VINSERTI128
(A) 或VINSERTF128
(C) 将零插入临时 YMM 寄存器。
版本 B:也编译为低效代码。将临时 YMM 寄存器归零,然后使用 将其与输入 YMM 寄存器混合VPBLENDD
。
没有什么理想的!
总之VMOVDQA
,如果您使用正确的代码序列,就有可能让 GCC 和 ICC 发出理想的指令。但是,我看不到任何方法可以让 Clang 或 MSVC 安全地发出VMOVDQA
指令。这些编译器缺少优化机会。
因此,在 Clang 和 MSVC 上,我们可以在 XOR+blend 和 XOR+insert 之间进行选择。哪个更好?我们转向Agner Fog 的指令表(也提供电子表格版本):
在 AMD 的 Ryzen 架构上:(Bulldozer 系列与__m256
这些的 AVX 等价物和挖掘机上的 AVX2 类似):
Instruction | Ops | Latency | Reciprocal Throughput | Execution Ports
---------------|-----|---------|-----------------------|---------------------
VMOVDQA | 1 | 0 | 0.25 | 0 (renamed)
VPBLENDD | 2 | 1 | 0.67 | 3
VINSERTI128 | 2 | 1 | 0.67 | 3
Agner Fog 似乎遗漏了他表格 Ryzen 部分中的一些 AVX2 指令。请参阅此 AIDA64 InstLatX64 结果以确认其VPBLENDD ymm
性能与VPBLENDW ymm
Ryzen 上的相同,而不是与VBLENDPS ymm
(可在 2 个端口上运行的 2 个微指令的 1c 吞吐量)相同。
另请参阅Excavator / Carrizo InstLatX64显示VPBLENDD
并VINSERTI128
在那里具有相同的性能(2 个周期延迟,每个周期 1 个吞吐量)。VBLENDPS
/相同VINSERTF128
。
在英特尔架构(Haswell、Broadwell 和 Skylake)上:
Instruction | Ops | Latency | Reciprocal Throughput | Execution Ports
---------------|-----|---------|-----------------------|---------------------
VMOVDQA | 1 | 0-1 | 0.33 | 3 (may be renamed)
VPBLENDD | 1 | 1 | 0.33 | 3
VINSERTI128 | 1 | 3 | 1.00 | 1
显然,VMOVDQA
在 AMD 和 Intel 上都是最优的,但我们已经知道,在 Clang 或 MSVC 上似乎都不是一个选项,直到它们的代码生成器被改进以识别上述惯用语之一或添加额外的内在函数为了这个确切的目的。
幸运的是,VPBLENDD
它至少与VINSERTI128
AMD 和 Intel CPU 一样好。在Intel处理器上,VPBLENDD
比. (事实上,它几乎与后者无法重命名的极少数情况一样好,除了需要一个全零向量常量。)如果你不能哄你的编译器,更喜欢导致指令的内在函数序列使用.VINSERTI128
VMOVDQA
VPBLENDD
VMOVDQA
如果您需要浮点数__m256
或__m256d
它的版本,则选择更加困难。在 Ryzen 上,VBLENDPS
吞吐量为 1c,但VINSERTF128
为 0.67c。在所有其他 CPU(包括 AMD Bulldozer 系列)上,VBLENDPS
性能相同或更好。它在英特尔上要好得多(与整数相同)。如果您专门针对 AMD 进行优化,您可能需要进行更多测试以查看在您的特定代码序列中哪个变体最快,否则混合。在 Ryzen 上只差一点点。
总之,针对通用 x86 并支持尽可能多的不同编译器,我们可以这样做:
#if (defined _MSC_VER)
ymm = _mm256_blend_epi32(_mm256_setzero_si256(),
ymm,
_MM_SHUFFLE(0, 0, 3, 3));
#elif (defined __INTEL_COMPILER)
ymm = _mm256_set_m128i(_mm_setzero_si128(), _mm256_castsi256_si128(ymm));
#elif (defined __GNUC__)
// Intended to cover GCC and Clang.
ymm = _mm256_inserti128_si256(_mm256_setzero_si256(),
_mm256_castsi256_si128(ymm),
0);
#else
#error "Unsupported compiler: need to figure out optimal sequence for this compiler."
#endif
在 Godbolt 编译器资源管理器上分别查看此版本和版本 A、B 和C。
也许您可以在此基础上定义您自己的基于宏的内在函数,直到出现更好的情况。