如何将两个__m128值合并为__m256?

22
我想将两个__m128值合并为一个__m256。 例如:
__m128 a = _mm_set_ps(1, 2, 3, 4);
__m128 b = _mm_set_ps(5, 6, 7, 8);

变成类似于这样的内容:

__m256 c = { 1, 2, 3, 4, 5, 6, 7, 8 };

有没有任何内置函数可以用来做到这一点?

5个回答

28
这应该能够满足你的需求:
__m128 a = _mm_set_ps(1,2,3,4);
__m128 b = _mm_set_ps(5,6,7,8);

__m256 c = _mm256_castps128_ps256(a);
c = _mm256_insertf128_ps(c,b,1);
如果你想要调换顺序,那么只需交换 ab
感兴趣的内置函数是_mm256_insertf128_ps,它可以让你将一个 128 位寄存器插入到 256 位 AVX 寄存器的上半部分或下半部分: http://software.intel.com/sites/products/documentation/studio/composer/en-us/2011/compiler_c/intref_cls/common/intref_avx_insertf128_ps.htm 它们的完整系列在这里:

1
某些版本的Visual Studio(肯定是2010年,可能还有一些较新的版本)在处理_mm256_castps128_ps256时存在一个错误,因此这段代码很可能会在这些版本上崩溃。请参见http://connect.microsoft.com/VisualStudio/feedback/details/653771/mm256-castps128-ps256-does-unaligned-read。如果您需要在这些编译器上运行您的代码,则需要使用用户user1584773提供的解决方案,将其替换为插入操作。 - peastman
1
请注意,这将导致结果为__m256{ 4, 3, 2, 1, 8, 7, 6, 5 }而不是__m256{ 1, 2, 3, 4, 5, 6, 7, 8 }。我认为OP想使用_mm_setr_ps而不是_mm_set_ps - plasmacel
1
如果你正在“插入”到下半部分,通常最好使用 _mm256_blend_ps 而不是 _mm256_insertf128_ps。延迟更低且在更多端口上运行。唯一的情况是 vinsertf128 可能比 vblendps ymm, ymm, imm8 更好的是使用内存源,用仅 16 字节的加载替换向量的低通道,而不是 32 字节的加载。 - Peter Cordes

7

英特尔文档提供了__m256 _mm256_set_m128(__m128 hi, __m128 lo)_mm256_setr_m128(lo, hi)两个内置函数,用于vinsertf128指令,这正是您需要的1。(当然,还有__m256d__m256i版本,它们使用相同的指令。如果支持AVX2,则__m256i版本可能使用vinserti128,否则也将使用f128。)

这些内在函数现在都被四大x86编译器(gcc、clang、MSVC和ICC)的最新版本所支持。但是旧版本不支持,就像英特尔文档中其他帮助器内在函数一样,广泛实施起来很慢。(通常情况下,GCC或Clang是最后一个没有你想用的东西的持有者)。
如果不需要对旧版本的GCC进行移植,可以使用它:这是表达您想要的内容最易读的方式,遵循众所周知的_mm_set和_mm_setr模式。
从性能上讲,它当然与手动转换+ vinsertf128内在函数(@Mysticial's answer)一样有效,至少对于GCC来说,这实际上就是内部.h如何实现_mm256_set_m128。 _mm256_set_m128 / _mm256_setr_m128的编译器版本支持:
  • clang: 3.6及以上版本(主线,不确定苹果是否包含)
  • GCC:8.x及以上版本,最近的GCC7中不存在!
  • ICC:至少从ICC13开始,在Godbolt上是最早的。
  • MSVC:至少从19.14和19.10(WINE)VS2015开始,在Godbolt上是最早的。

https://godbolt.org/z/1na1qr 包含所有4个编译器的测试用例。

__m256 combine_testcase(__m128 hi, __m128 lo) {
    return _mm256_set_m128(hi, lo);
}

他们都将这个函数编译为一个vinsertf128,只有MSVC例外,即使是最新版本也会浪费一个vmovups xmm2, xmm1复制寄存器。(我使用了-O2 -Gv -arch:AVX来使用矢量调用约定,以便参数在寄存器中,从而可以为MSVC创建一个高效的非内联函数定义。)假设MSVC可以将结果写入第三个寄存器,那么将其内联到更大的函数中应该是可以的,而不是调用约定强制它读取xmm0并写入ymm0。
注脚1:
vinsertf128 在 Zen1 上非常高效,在其他具有 256 位宽派生单元的 CPU 上与 vperm2f128 一样高效。它还可以从内存中获取高半部分,以防编译器将其溢出或将 _mm_loadu_ps 折叠到其中,而无需单独进行 128 位加载到寄存器中;vperm2f128 的内存操作数将是一个 256 位加载,这是不希望发生的。

https://uops.info/ / https://agner.org/optimize/


2

还可以使用 permute 内置函数:

__m128 a = _mm_set_ps(1,2,3,4);
__m128 b = _mm_set_ps(5,6,7,8);
__m256 c = _mm256_permute2f128_ps(_mm256_castps128_ps256(a), _mm256_castps128_ps256(b), 0x20);

我不知道哪种方式更快。


如果它实际上编译成了vperm2f128,那么在Zen1上它会更慢,在Intel上也没有比vinsertf128更好的优势。 - Peter Cordes

2
即使这个也可以工作:
__m128 a = _mm_set_ps(1,2,3,4);
__m128 b = _mm_set_ps(5,6,7,8);

__m256 c = _mm256_insertf128_ps(c,a,0);
c = _mm256_insertf128_ps(c,b,1);

由于未初始化c,您将收到警告,但可以忽略它。如果您正在寻找性能,此解决方案将使用比其他解决方案更少的时钟周期。


2
你确定这比Mystical提出的解决方案更快吗?据我所知,castps128_ps256是免费的,不是吗? 此外,我的应用程序极大地受益于使用cast而不是insert(同样适用于extract)。 - user1829358
@user1829358:低插入(low insert)有望被优化掉,但没有必要让编译器去删除不必要的东西。此外,它通过读取尚未初始化的“c”具有未定义的行为,因此我强烈建议不要这样做。是的,显然转换更好;在汇编中转换是免费的,你只需要一个“vinsertf128”指令。 - Peter Cordes

0

我相信这是最简单的:

#define _mm256_set_m128(/* __m128 */ hi, /* __m128 */ lo) \ _mm256_insertf128_ps(_mm256_castps128_ps256(lo), (hi), 0x1)

__m256 c = _mm256_set_m128(a, b);

请注意,如果您#include "immintrin.h" ,则__mm256_set_m128已在msvc 2019中定义。


Intel 文档 _mm256_set_m128(__m128 hi, __m128 lo) - 你应该只使用它,而不是自己定义。 - Peter Cordes
正确的,它应该已经被定义了,但是如果你使用的是旧版本的MSVC,它可能没有被定义。 - J. Tully

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接