有些问题的标题相似,但是我的问题与一个非常特殊的用例有关,而其他地方没有涉及。
我有4个__128d寄存器(x0,x1,x2,x3),我想按如下方式在5个__256d寄存器(y0,y1,y2,y3,y4)中重新组合它们的内容,以准备进行其他计算:>
on entry:
x0 contains {a0, a1}
x1 contains {a2, a3}
x2 contains {a4, a5}
x3 contains {a6, a7}
on exit:
y0 contains {a0, a1, a2, a3}
y1 contains {a1, a2, a3, a4}
y2 contains {a2, a3, a4, a5}
y3 contains {a3, a4, a5, a6}
y4 contains {a4, a5, a6, a7}
我在下面的实现非常缓慢。有更好的方法吗?
y0 = _mm256_set_m128d(x1, x0);
__m128d lo = _mm_shuffle_pd(x0, x1, 1);
__m128d hi = _mm_shuffle_pd(x1, x2, 1);
y1 = _mm256_set_m128d(hi, lo);
y2 = _mm256_set_m128d(x2, x1);
lo = hi;
hi = _mm_shuffle_pd(x2, x3, 1);
y3 = _mm256_set_m128d(hi, lo);
y4 = _mm256_set_m128d(x3, x2);
答案 0 :(得分:6)
使用寄存器中的输入,您可以按照5个随机播放指令进行操作:
vinsertf128
通过分别连接2个xmm寄存器来创建y0,y2和y4。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);
}
很好地(with gcc and clang -O3 -march=haswell
on Godbolt)编译为:
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编译器资源管理器链接中。)
这在AMD上非常好[em] ,其中vinsertf128
非常便宜(因为256位寄存器被实现为2个128位的一半,所以它只是128位的副本不需要特殊的shuffle端口。)在AMD上,256位穿越交叉的速度很慢,但是像vshufpd
这样的车道内256位shuffle仅为2 oups。
在Intel上,这是相当不错的,但是带有AVX的主流Intel CPU在256位或FP随机播放中每个时钟的随机播放吞吐量仅为1。 (Sandybridge和更早版本的整数128位洗牌具有更高的吞吐量,但是AVX2 CPU放弃了多余的洗牌单元,反正它们对此毫无帮助。)
因此,英特尔CPU根本无法利用指令级并行性,但是总共只有5微妙。那是最小的,因为您需要5个结果。
但是特别是如果周围的代码也出现洗牌瓶颈时,值得考虑的存储/重装策略只有4个存储和5个重叠的矢量加载。或者也许2x vinsertf128
来构造y0
和y4
,然后2x 256位存储+ 3个重叠的重载。这样一来,杂乱无章的执行者就可以仅使用y0
或y4
来执行相关指令,而将存储转发停顿解析为y1.3。
尤其是如果您不太在意英特尔第一代Sandybridge,那么未对齐的256位矢量加载效率会降低。 (请注意,如果您使用的是GCC,则希望使用gcc -mtune=haswell
进行编译以关闭-mavx256-split-unaligned-load
的默认/沙桥调整。无论使用哪种编译器,-march=native
都是一个好主意如果要让二进制文件在编译它的机器上运行,则可以充分利用指令集和设置调整选项。)
但是如果前端的总uop吞吐量更多地位于瓶颈所在,那么改组实现最好。
(有关性能调优的更多信息,请参见https://agner.org/optimize/和x86 tag wiki中的其他性能链接。还有What considerations go into predicting latency for operations on modern superscalar processors and how can I calculate them by hand?,但实际上Agner Fog的指南是更深入的指南,解释了什么吞吐量与延迟之间的关系实际上是大约。)
我什至不需要保存,因为连续内存中也已经有数据。
然后简单地将其加载5次重叠加载几乎可以肯定是您可以执行的最有效的操作。
Haswell的每个时钟从L1d可以执行2次加载,如果跨越缓存行边界,则可以进行更少的加载。 因此,如果您可以将块对齐64,则完全没有缓存行拆分就可以了。缓存未命中的速度很慢,但是从L1d缓存中重新加载热数据的成本非常低,并且现代CPU具有通常,AVX支持具有有效的不对齐负载支持。
(就像我之前说的,如果使用gcc,请确保您使用-march=haswell
或-mtune=haswell
而不是-mavx
进行编译,以避免gcc的-mavx256-split-unaligned-load
。)
4个负载+ 1 vshufpd
(y0,y2)可能是一种平衡负载端口压力和ALU压力的好方法,具体取决于周围代码中的瓶颈。如果周围代码对随机端口压力较低,甚至3个负载+ 2次随机播放。
它们在以前计算的寄存器中,要求将它们加载。
如果先前的计算仍将源数据保存在寄存器中,那么您可以首先进行256位加载,而仅将其128位低半用于较早的计算。( XMM寄存器是相应YMM寄存器的低128,读取它们不会干扰高通道,因此_mm256_castpd256_pd128
会编译为0个asm指令。)
对y0,y2和y4进行256位加载,并将它们的下半部分用作x0,x1和x2。 (稍后将y1和y3构造成没有对齐的负载或随机播放)。
仅x3还不是您想要的256位向量的低128位。
理想情况下,当您从同一地址执行_mm_loadu_pd
和_mm256_loadu_pd
时,编译器已经注意到了这种优化,但是您可能需要通过这样做来保持
__m256d y0 = _mm256_loadu_pd(base);
__m128d x0 = _mm256_castpd256_pd128(y0);
等,并根据周围的代码提取ALU内部(_mm256_extractf128_pd
)或x3
的128位负载。如果只需要一次,则最好将其折叠为用于任何指令的内存操作数。
潜在的缺点:128位计算开始之前的等待时间会稍长一些;如果256位负载是高速缓存行交叉而没有128位负载,则延迟几个周期。但是,如果您的数据块按64字节对齐,则不会发生这种情况。