通过寄存器中的输入,您可以通过 5 个随机指令来完成:
- 3x
vinsertf128
通过连接 2 个 xmm 寄存器来创建 y0、y2 和 y4。
- 2x
vshufpd
(车道内洗牌)在这些结果之间创建 y1 和 y3。
请注意,y0 和 y2 的低通道包含 a1 和 a2,它们是 y1 的低通道所需的元素。同样的洗牌也适用于高车道。
#include <immintrin.h>
void merge(__m128d x0, __m128d x1, __m128d x2, __m128d x3,
__m256d *__restrict y0, __m256d *__restrict y1,
__m256d *__restrict y2, __m256d *__restrict y3, __m256d *__restrict y4)
{
*y0 = _mm256_set_m128d(x1, x0);
*y2 = _mm256_set_m128d(x2, x1);
*y4 = _mm256_set_m128d(x3, x2);
// take the high element from the first vector, low element from the 2nd.
*y1 = _mm256_shuffle_pd(*y0, *y2, 0b0101);
*y3 = _mm256_shuffle_pd(*y2, *y4, 0b0101);
}
编译得很好(与 gcc 和 clang-O3 -march=haswell在戈德螺栓上 https://godbolt.org/#z:OYLghAFBqd5QCxAYwPYBMCmBRdBLAF1QCcAaPECAM1QDsCBlZAQwBtMQBGAOgAYB2ACwBOAKyCATAGZhsucIAcpAFZdSrZrVDIApBIBCe/aQDOqAK7FkHAOR6peWslbmsAah1SAwngC2vxwJiR24ET2wdXgBBSKiAN1Q8dDdfTGJgTAgAfSzfTgkFZIAPXlI3HLyC4s4yivzCtyKJWtz64qlSWLdu7oqJUQA2ZIAqHOJMEyC8ZAI3AE9S8tz%2BobdRrPHJ4Jn5mq6epd8VkbGJqZ255sPjtdOt6dm5juvBk42z7cfBAEpYnX5DNEDsMFh4pAARcr%2BFZZEyYAitKoQIo1Rq8X5SQFRYGXMGQ3JHQaw%2BGIwrIq4ojFYnGCPFQwkDYkIypkorPJpUv5AnoAeh5bgIzAA1pgBQhRQg8MAEG5MOxUvQ3FRiKhfGLRVQ8MRJm44pgZiQyqxUAB3WXyzCK5Wq9VuCS0dDcfbdEGcOkEmEmBDmKhUdhZAAO6AgIMWIKuvAARrxODHOdyXU93dCiV6fX7MIHg%2BGyiDBGUozG454sf9wVz4olkql0pnUKxg3UqmiWizqq22o0rk2GmzOgnestXrd3vcLos%2BsP1ptzo89gOXqtpx8HvNu0Ol3dZ/NnpPN6Pt3Mfn8Ac61qDPPiU4y4cy2sjUSV4zEBz3ksbk75Yd7ff6g8jFhRMpOGfA43zcSVP2/dM/2DICu2A0CeldT9PRJNsIElI1UGfM9w1Q1N0PvJoykpEsKwOD9LwgvByIHSDqIJaDf0zf8SMaZ4QLo7FkKTRjryZUlgywtxjVwgc8wIm8iKRPsu1w/hwRsb51BAGxRBsUhaDU3hNNQNSvCMIw3DMSxrA8aROE0ggdOUlShRAQRBG4KRRFEYReGEAZhEENzRE4TgBlUmxBE07SbF00h9JsTSTBAUobIi5TSDgWAkDQXwAzwdgyAoCAMqynKQGABRmk1VgCDSOKIEjWzSEjRxmGIOY1Ks0gMoVAgAHlaFYFqktILBfE0YB2Dq/BxhmPA9TigbMCKfVzEq1rNMCOU6qmXwVpUmh6CYNgOB4AQRHEaR5DkJQNC0FAjIMdQ8EjOLIBU1AAwIPA6FmgBaLqpDcL6AHU2FYf7hqsMIIQQZgTBNOVWFiiwrEOlTWDUjStLq6KigUAYvoGWlgGQZA3AUbgJDcCBcEIEgLKkVEvFVQq0lpkC3EMgwjGs2zvnskACj4AZ8l4OmpBkfh%2BGEKR%2BGC0KMYG6LYvi0hEt0lTUsQFBGeytJyEoArteIFAruAfheFKcrKu1ShaoGhraCa/q2o6y1ut6/rIqGkaxoGib9Xema6vmxblpsNq1tRgbNu20hdsYFgxqOoQxEkGRzsUdQRpujm7tYB6nogF63o%2B2hvt%2B/6gdYEGvrB5AIfBKGYbhhGzOR4L0fCyKsZxvHaWcEa3H4Pg%2BApqmiGIFmygZzKDdpiRvjZ279C5pKedIByJEH0RSoUBR%2BE4PfBAGbzxBlsLMbUxWEu5lG1IkM/5Yv5Xr9IPVtWLxygA) to:
merge(double __vector(2), double __vector(2), double __vector(2), double __vector(2), double __vector(4)*, double __vector(4)*, double __vector(4)*, double __vector(4)*, double __vector(4)*):
vinsertf128 ymm0, ymm0, xmm1, 0x1
vinsertf128 ymm3, ymm2, xmm3, 0x1
vinsertf128 ymm1, ymm1, xmm2, 0x1
# vmovapd YMMWORD PTR [rdi], ymm0
vshufpd ymm0, ymm0, ymm1, 5
# vmovapd YMMWORD PTR [rdx], ymm1
vshufpd ymm1, ymm1, ymm3, 5
# vmovapd YMMWORD PTR [r8], ymm3
# vmovapd YMMWORD PTR [rsi], ymm0
# vmovapd YMMWORD PTR [rcx], ymm1
# vzeroupper
# ret
我注释掉了内联时会消失的存储和内容,因此我们实际上只有 5 个随机播放指令,而不是您问题中的代码的 9 个随机播放指令。 (也包含在 Godbolt 编译器资源管理器链接中)。
This is very擅长 AND,其中vinsertf128
是超级便宜的(因为 256 位寄存器被实现为 2x 128 位一半,所以它只是一个 128 位副本,不需要特殊的随机播放端口。)256 位通道交叉随机播放在 AMD 上很慢,但在 - Lane 256 位随机播放,例如vshufpd
只是 2 uop。
在 Intel 上,这相当不错,但具有 AVX 的主流 Intel CPU 对于 256 位或 FP shuffle 仅具有 1 个每时钟 shuffle 吞吐量。 (Sandybridge 和更早版本的整数 128 位随机播放具有更高的吞吐量,但 AVX2 CPU 放弃了额外的随机播放单元,而且它们对此没有任何帮助。)
因此,Intel CPU 根本无法利用指令级并行性,但总共只有 5 uops,这很好。这是可能的最小值,因为您需要 5 个结果。
但特别是如果周围的代码也在洗牌上遇到瓶颈,值得考虑仅包含 4 个存储和 5 个重叠向量加载的存储/重新加载策略。或者也许 2xvinsertf128
构建y0
and y4
,然后 2x 256 位存储 + 3 次重叠重新加载。这可以让无序执行仅使用依赖指令开始y0
or y4
而存储转发停顿已解决 y1..3。
特别是如果您不太关心英特尔第一代 Sandybridge,其中未对齐的 256 位矢量加载效率较低。 (请注意,您需要使用以下命令进行编译gcc -mtune=haswell
关闭-mavx256-split-unaligned-load
如果您使用的是 GCC,则默认/sandybridge 调整。无论编译器如何,-march=native
如果使二进制文件在编译它的机器上运行,以充分利用指令集并设置调整选项,这是一个好主意。)
但如果前端的 uop 总吞吐量更多是瓶颈所在,那么 shuffle 实现是最好的。
(See https://agner.org/optimize/ https://agner.org/optimize/以及其他性能链接x86 标签维基 https://stackoverflow.com/tags/x86/info有关性能调整的更多信息。还预测现代超标量处理器上的操作延迟需要考虑哪些因素以及如何手动计算它们? https://stackoverflow.com/questions/51607391/what-considerations-go-into-predicting-latency-for-operations-on-modern-supersca,但实际上 Agner Fog 的指南是一个更深入的指南,它解释了吞吐量与延迟的实际含义。)
我什至不需要保存,因为数据也已经在连续内存中可用。
然后,简单地加载 5 个重叠负载几乎肯定是您可以做的最有效的事情。
Haswell 每个时钟可以从 L1d 执行 2 次加载,或者当任何跨越缓存线边界时加载更少。因此,如果您可以将块对齐 64,那么它的效率非常高,根本不需要缓存行分割。缓存未命中速度很慢,但从 L1d 缓存重新加载热数据非常便宜,并且支持 AVX 的现代 CPU 通常具有高效的未对齐加载支持。
(就像我之前说的,如果使用 gcc 请确保使用-march=haswell
or -mtune=haswell
, 不只是-mavx
,以避免 gcc 的-mavx256-split-unaligned-load
.)
4 负载 + 1vshufpd
(y0, y2) 可能是平衡负载端口压力与 ALU 压力的好方法,具体取决于周围代码中的瓶颈。如果周围代码的 shuffle 端口压力较低,甚至可以是 3 个加载 + 2 个 shuffle。
它们位于先前计算的寄存器中,需要加载它们。
如果先前的计算仍然在寄存器中包含源数据,则您可以首先完成 256 位加载,然后仅将其 128 位低半部分用于先前的计算。(XMM 寄存器是相应 YMM 寄存器的低 128,读取它们不会干扰上层通道,因此_mm256_castpd256_pd128
编译为零 asm 指令。)
对 y0、y2 和 y4 进行 256 位加载,并将它们的低半部分用作 x0、x1 和 x2。 (稍后使用未对齐的负载或洗牌构造 y1 和 y3)。
只有 x3 还不是您想要的 256 位向量的低 128 位。
理想情况下,当您执行以下操作时,编译器已经注意到这种优化_mm_loadu_pd
and a _mm256_loadu_pd
来自同一个地址,但可能你需要通过这样做来手持它
__m256d y0 = _mm256_loadu_pd(base);
__m128d x0 = _mm256_castpd256_pd128(y0);
等等,以及提取 ALU 内在函数 (_mm256_extractf128_pd
)或 128 位负载x3
,取决于周围的代码。如果只需要一次,那么最好将其折叠到内存操作数中,以供任何指令使用。
潜在的缺点:128 位计算开始之前的延迟稍高,或者如果 256 位负载与 128 位负载不交叉的缓存行交叉,则需要几个周期。但如果您的数据块按 64 字节对齐,则不会发生这种情况。