更新:现在有一个 __m128i _mm256_zextsi128_si256(__m128i) 内在函数;见Agner Fog's answer。下面的其余答案仅与不支持此内在函数的旧编译器相关,并且没有高效、可移植的解决方案。
不幸的是,理想的解决方案将取决于您使用的编译器,而对于其中一些编译器,没有理想的解决方案。
我们可以用几种基本的方式来写这个:
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 位,因此可以安全地使用它们中的任何一个。但哪个是最优化的?好吧,这取决于您使用的是哪个编译器...
GCC:
版本 A:根本不支持,因为 GCC 缺少 _mm256_set_m128i 内在函数。 (当然可以模拟,但可以使用“B”或“C”中的一种形式来完成。)
版本 B:编译为低效代码。成语不被识别,内在函数被非常字面地翻译成机器代码指令。使用 VPXOR 将临时 YMM 寄存器归零,然后使用 VPBLENDD 将其与输入 YMM 寄存器混合。
版本 C:理想。尽管代码看起来有点吓人且效率低下,但所有支持 AVX2 代码生成的 GCC 版本都可以识别这个习语。你得到了预期的VMOVDQA xmm?, xmm? 指令,它隐式地清除了高位。
更喜欢 C 版!
叮当:
版本 A:编译为低效代码。使用 VPXOR 将临时 YMM 寄存器归零,然后使用 VINSERTI128(或浮点形式,取决于版本和选项)将其插入到临时 YMM 寄存器中。
版本 B 和 C:也编译为低效代码。临时 YMM 寄存器再次清零,但在这里,它使用 VPBLENDD 与输入 YMM 寄存器混合。
没有什么理想的!
国际商会:
版本 A:理想。产生预期的VMOVDQA xmm?, xmm? 指令。
版本 B:编译为低效代码。将临时 YMM 寄存器归零,然后将零与输入 YMM 寄存器 (VPBLENDD) 混合。
版本 C:也编译为低效代码。将临时 YMM 寄存器归零,然后使用 VINSERTI128 将零插入临时 YMM 寄存器。
更喜欢 A 版!
MSVC:
版本 A 和 C:编译为低效代码。将临时 YMM 寄存器清零,然后使用 VINSERTI128 (A) 或 VINSERTF128 (C) 将零插入临时 YMM 寄存器。
版本 B:也编译为低效代码。将临时 YMM 寄存器归零,然后使用 VPBLENDD 将其与输入 YMM 寄存器混合。
没有什么理想的!
总之,如果使用正确的代码序列,GCC 和 ICC 可以发出理想的VMOVDQA 指令。但是,我看不到任何方法可以让 Clang 或 MSVC 安全地发出 VMOVDQA 指令。这些编译器错过了优化机会。
因此,在 Clang 和 MSVC 上,我们可以在 XOR+blend 和 XOR+insert 之间进行选择。哪个更好?我们转向Agner Fog's instruction tables(电子表格版本also available):
在 AMD 的 Ryzen 架构上:(Bulldozer 系列与 AVX __m256 等价物类似,对于 AVX2 on Excavator):
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 指令。请参阅 this AIDA64 InstLatX64 result 以确认 VPBLENDD ymm 在 Ryzen 上的性能与 VPBLENDW ymm 相同,而不是与 VBLENDPS ymm 相同(1c 吞吐量来自可以在 2 个端口上运行的 2 微指令)。
另请参见 an 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 在 AMD 和 Intel CPU 上至少与VINSERTI128 一样好。在 Intel 处理器上,VPBLENDD 是对VINSERTI128 的显着改进。 (事实上,它几乎和VMOVDQA 一样好,在后者无法重命名的极少数情况下,除了需要一个全零向量常量。)如果可以的话,更喜欢导致VPBLENDD 指令的内在函数序列不要哄你的编译器使用VMOVDQA。
如果你需要一个浮点的__m256 或__m256d 这个版本,选择就比较困难了。在 Ryzen 上,VBLENDPS 的吞吐量为 1c,但VINSERTF128 的吞吐量为 0.67c。在所有其他 CPU(包括 AMD Bulldozer 系列)上,VBLENDPS 等于或更好。它在 Intel 上更好(与整数相同)。如果您专门针对 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
请分别查看此版本和版本 A、B 和 C on the Godbolt compiler explorer。
也许您可以在此基础上定义自己的基于宏的内在函数,直到出现更好的情况。