有没有更有效的方法将4个连续的双打广播到4个YMM寄存器中?

时间:2014-05-13 13:54:34

标签: gcc intel simd intrinsics avx

在一段C ++代码中,它执行类似于(但不完全是)矩阵乘法的操作,我将4个连续的双精度加载到4个YMM寄存器中,如下所示:

# a is a 64-byte aligned array of double
__m256d b0 = _mm256_broadcast_sd(&b[4*k+0]);
__m256d b1 = _mm256_broadcast_sd(&b[4*k+1]);
__m256d b2 = _mm256_broadcast_sd(&b[4*k+2]);
__m256d b3 = _mm256_broadcast_sd(&b[4*k+3]);

我在Sandy Bridge机器上使用gcc-4.8.2编译了代码。硬件事件计数器(Intel PMU)表明CPU实际上从L1缓存发出4个单独的负载。虽然在这一点上我不受L1延迟或带宽的限制,但我很有兴趣知道是否有办法用一个256位负载(或两个128位负载)加载4个双打并将它们混入4个YMM寄存器。我查看了Intel Intrinsics Guide,但找不到完成所需洗牌的方法。这可能吗?

(如果CPU没有合并4次连续加载的前提实际上是错误的,请告诉我。)

3 个答案:

答案 0 :(得分:3)

TL; DR:使用_mm256_set1_pd()进行四次广播加载几乎总是最好的。 这对Haswell及以后非常好,vbroadcastsd ymm,[mem]不需要ALU随机播放,而且通常也是Sandybridge / Ivybridge的最佳选择(它是2-uop load + shuffle指令)。

这也意味着除了double的自然对齐之外,你根本不需要关心对齐。

第一个向量比你执行两步加载+随机播放更早就准备好了,因此当第一个向量仍在加载时,可能会使用这些向量开始无序执行代码。 AVX512甚至可以将广播加载折叠到ALU指令的存储器操作数中,因此这样做可以让重新编译略微利用AVX512和256b向量。

(通常最好使用set1(x),而不是_mm256_broadcast_sd(&x);如果vbroadcastsd的仅AVX2注册源形式不可用,编译器可以选择存储 - &gt ;广播加载或做两次shuffle。你永远不知道内联什么时候意味着你的代码将在已经在寄存器中的输入上运行。)

如果您在加载端口资源冲突或吞吐量方面确实存在瓶颈,而不是总uop或ALU / shuffle资源,则可能有助于用16B-> 32B广播替换一对64-> 256b广播-load(vbroadcastf128 / _mm256_broadcast_ p d)和两次内线随机播放(vpermilpdvunpckl/hpd({{1 }}))。

或使用AVX2 :加载32B并使用4 _mm256_shuffle_pd shuffle将每个元素广播到单独的向量中。

来源Agner Fog's insn tables (and microarch pdf)

英特尔Haswell及其后期:

_mm256_permute4x64_pd和其他广播加载insn是1uop指令,完全由加载端口处理(广播“免费”发送)。

以这种方式进行四次广播加载的总成本是4条指令。融合域:4uops。 unfused-domain:p2 / p3为4 uops。吞吐量:每个周期两个向量。

Haswell在port5上只有一个shuffle单元。使用load + shuffle执行所有广播加载将成为p5的瓶颈。

最大广播吞吐量可能混合使用vbroadcastsd ymm,[mem]和随机播放:

vbroadcastsd ymm,m64

这些寻址模式中的任何一种都可以是双寄存器索引寻址模式,因为they don't need to micro-fuse to be a single uop

AVX1:每2个循环5个载体,使p2 / p3和p5饱和。 (忽略16B负载上的缓存行拆分)。 6个融合域uops,每2个周期只留下2个uop来使用5个向量...真正的代码可能会使用一些负载吞吐量来加载其他东西(例如,来自另一个阵列的非广播32B负载,可能是内存操作数到ALU指令),或为商店留出空间来窃取p23而不是使用p7。

## Haswell maximum broadcast throughput with AVX1
vbroadcastsd    ymm0, [rsi]
vbroadcastsd    ymm1, [rsi+8]
vbroadcastf128  ymm2, [rsi+16]     # p23 only on Haswell, also p5 on SnB/IvB
vunpckhpd       ymm3, ymm2,ymm2
vunpcklpd       ymm2, ymm2,ymm2
vbroadcastsd    ymm4, [rsi+32]     # or vaddpd ymm0, [rdx+something]
#add             rsi, 40

AVX2:每4个循环11个载体,使p23和p5饱和。 (忽略32B加载的缓存行拆分...)。融合域:12个uop,每4个循环剩余2个uop。

我认为32B未对齐的负载在性能方面比## Haswell maximum broadcast throughput with AVX2 vmovups ymm3, [rsi] vbroadcastsd ymm0, xmm3 # special-case for the low element; compilers should generate this from _mm256_permute4x64_pd(v, 0) vpermpd ymm1, ymm3, 0b01_01_01_01 # NASM syntax for 0x99 vpermpd ymm2, ymm3, 0b10_10_10_10 vpermpd ymm3, ymm3, 0b11_11_11_11 vbroadcastsd ymm4, [rsi+32] vbroadcastsd ymm5, [rsi+40] vbroadcastsd ymm6, [rsi+48] vbroadcastsd ymm7, [rsi+56] vbroadcastsd ymm8, [rsi+64] vbroadcastsd ymm9, [rsi+72] vbroadcastsd ymm10,[rsi+80] # or vaddpd ymm0, [rdx + whatever] #add rsi, 88 等未对齐的16B负载更脆弱。

Intel SnB / IvB:

vbroadcastf128是2个融合域uops:p5(shuffle)和p23(load)。

vbroadcastsd ymm, m64vbroadcastss xmm, m32仅为单uop加载端口。有趣的是,movddup xmm, m64也是一个单uop加载端口指令,但与所有256b负载一样,它占用一个加载端口2个周期。它仍然可以在第二个周期生成存储地址。但是,对于未对齐的32B负载,此uarch不能很好地处理缓存行拆分。 gcc默认使用movups / vinsertf128加载vmovddup ymm, m256 / -mtune=sandybridge的未对齐32B加载。

4x广播加载:8个融合域uops:4个p5和4个p23。吞吐量:每4个周期4个向量,在端口5上出现瓶颈。同一个周期中来自同一高速缓存行的多个负载不会导致缓存库冲突,因此这远远不能使加载端口饱和(存储地址也需要)代)。这只发生在同一周期中两个不同缓存行的同一个存储区中。

如果uop-cache很冷,那么解码器的最坏情况是多个2-uop指令而没有其他指令,但是好的编译器会在它们之间混合使用单uop指令。

SnB有两个shuffle单元,但只有p5上的一个可以处理AVX中具有256b版本的shuffle。使用p1 integer-shuffle uop向xmm寄存器的两个元素广播double不会让我们无处可去,因为-mtune=ivybridge需要p5 shuffle uop。

vinsertf128 ymm,ymm,xmm,i

每个时钟一个,饱和p5但仅使用p23容量的一半。

我们可以节省一个负载uop,代价是2个shuffle uops,吞吐量=每3个时钟两个结果:

## Sandybridge maximum broadcast throughput: AVX1
vbroadcastsd    ymm0, [rsi]
add             rsi, 8

执行32B加载并使用2x vbroadcastf128 ymm2, [rsi+16] # 2 uops: p23 + p5 on SnB/IvB vunpckhpd ymm3, ymm2,ymm2 # 1 uop: p5 vunpcklpd ymm2, ymm2,ymm2 # 1 uop: p5 - >解压缩如果商店是竞争p23的一部分,4x vperm2f128可能会有所帮助。

答案 1 :(得分:2)

在我的matrix multiplication code中我只需要为每个内核代码使用一次广播,但是如果你真的想在一条指令中加载四个双打然后将它们广播到四个寄存器,你就可以这样做

#include <stdio.h>
#include <immintrin.h>

int main() {
    double in[] = {1,2,3,4};
    double out[4];
    __m256d x4 = _mm256_loadu_pd(in);
    __m256d t1 = _mm256_permute2f128_pd(x4, x4, 0x0);
    __m256d t2 = _mm256_permute2f128_pd(x4, x4, 0x11);
    __m256d broad1 = _mm256_permute_pd(t1,0);
    __m256d broad2 = _mm256_permute_pd(t1,0xf);
    __m256d broad3 = _mm256_permute_pd(t2,0);
    __m256d broad4 = _mm256_permute_pd(t2,0xf);

    _mm256_storeu_pd(out,broad1);   
    printf("%f %f %f %f\n", out[0], out[1], out[2], out[3]);
    _mm256_storeu_pd(out,broad2);   
    printf("%f %f %f %f\n", out[0], out[1], out[2], out[3]);
    _mm256_storeu_pd(out,broad3);   
    printf("%f %f %f %f\n", out[0], out[1], out[2], out[3]);
    _mm256_storeu_pd(out,broad4);   
    printf("%f %f %f %f\n", out[0], out[1], out[2], out[3]);
}

编辑:这是另一种基于Paul R建议的解决方案。

__m256 t1 = _mm256_broadcast_pd((__m128d*)&b[4*k+0]);
__m256 t2 = _mm256_broadcast_pd((__m128d*)&b[4*k+2]);
__m256d broad1 = _mm256_permute_pd(t1,0);
__m256d broad2 = _mm256_permute_pd(t1,0xf);
__m256d broad3 = _mm256_permute_pd(t2,0);
__m256d broad4 = _mm256_permute_pd(t2,0xf);

答案 2 :(得分:1)

这是基于Z Boson原始答案(编辑前)的变体,使用两个128位加载而不是一个256位加载。

__m256d b01 = _mm256_castpd128_pd256(_mm_load_pd(&b[4*k+0]));
__m256d b23 = _mm256_castpd128_pd256(_mm_load_pd(&b[4*k+2]));
__m256d b0101 = _mm256_permute2f128_pd(b01, b01, 0);
__m256d b2323 = _mm256_permute2f128_pd(b23, b23, 0);
__m256d b0000 = _mm256_permute_pd(b0101, 0);
__m256d b1111 = _mm256_permute_pd(b0101, 0xf);
__m256d b2222 = _mm256_permute_pd(b2323, 0);
__m256d b3333 = _mm256_permute_pd(b2323, 0xf);

在我的情况下,这比使用一个256位加载稍快,可能是因为第一个置换可以在第二个128位加载完成之前启动。


编辑:gcc将两个加载和前两个加载编译成

vmovapd (%rdi),%xmm8
vmovapd 0x10(%rdi),%xmm4
vperm2f128 $0x0,%ymm8,%ymm8,%ymm1
vperm2f128 $0x0,%ymm4,%ymm4,%ymm2

Paul R建议使用_mm256_broadcast_pd()可以写成:

__m256d b0101 = _mm256_broadcast_pd((__m128d*)&b[4*k+0]);
__m256d b2323 = _mm256_broadcast_pd((__m128d*)&b[4*k+2]);

编译成

vbroadcastf128 (%rdi),%ymm6
vbroadcastf128 0x10(%rdi),%ymm11

并且比做两个vmovapd + vperm2f128(已测试)更快。

在我的代码中,它由向量执行端口而不是L1缓存访问绑定,这仍然比4 _mm256_broadcast_sd()略慢,但我认为L1带宽受限的代码可以从中受益匪浅。