SIMD减少4个向量而没有hadd

问题描述 投票:2回答:1

我正在尝试优化一些代码,并且我处于4个向量__m256d的状态,我想将每个向量的总和存储在另一个__m256d中。所以基本上是result = [sum(a), sum(b), sum(c), sum(d)]。我知道有一种方法可以使用2个hadd进行混合和置换,但是我意识到hadd太昂贵了。

因此,我想知道是否有一个内部函数可以更快地执行此操作。

c++ c simd intrinsics
1个回答
1
投票

让编译器为您优化此设置。使用gcc矢量扩展名*,在转置矩阵上求和的代码可能如下所示:

#include <stdint.h>

typedef uint64_t v4u64 __attribute__((vector_size(32)));
typedef double v4f64  __attribute__((vector_size(32)));

v4f64 dfoo(v4f64 sv0, v4f64 sv1, v4f64 sv2, v4f64 sv3)
{
  v4f64 tv[4];
  tv[0] = __builtin_shuffle(sv0, sv1, (v4u64){0,4,2,6});
  tv[1] = __builtin_shuffle(sv0, sv1, (v4u64){1,5,3,7});
  tv[2] = __builtin_shuffle(sv2, sv3, (v4u64){0,4,2,6});
  tv[3] = __builtin_shuffle(sv2, sv3, (v4u64){1,5,3,7});
  v4f64 fv[4];
  fv[0] = __builtin_shuffle(tv[0], tv[2], (v4u64){0,1,4,5});
  fv[1] = __builtin_shuffle(tv[0], tv[2], (v4u64){2,3,6,7});
  fv[2] = __builtin_shuffle(tv[1], tv[3], (v4u64){0,1,4,5});
  fv[3] = __builtin_shuffle(tv[1], tv[3], (v4u64){2,3,6,7});
  return fv[0]+fv[1]+fv[2]+fv[3];
}

gcc-9.2.1产生以下程序集:

dfoo:
    vunpcklpd   %ymm3, %ymm2, %ymm5
    vunpcklpd   %ymm1, %ymm0, %ymm4
    vunpckhpd   %ymm1, %ymm0, %ymm0
    vinsertf128 $1, %xmm5, %ymm4, %ymm1
    vperm2f128  $49, %ymm5, %ymm4, %ymm4
    vunpckhpd   %ymm3, %ymm2, %ymm2
    vaddpd  %ymm4, %ymm1, %ymm1
    vinsertf128 $1, %xmm2, %ymm0, %ymm3
    vperm2f128  $49, %ymm2, %ymm0, %ymm0
    vaddpd  %ymm3, %ymm1, %ymm1
    vaddpd  %ymm0, %ymm1, %ymm0
    ret

Agner Fog的桌子说:

  • vunpck[h/l]pd:1个周期延迟,每个周期吞吐量1个,1个uOP端口5。
  • [vinsertf128:3个周期的延迟,每个周期1个吞吐量,1个uOP端口5。
  • [vperm2f128:3个周期的延迟,每个周期1个吞吐量,1个uOP端口5。
  • [vaddpd:4个周期的延迟,每个周期2个吞吐量,1个uOP port01。

总共有

  • 4 [解压] + 2 [插入] + 2 [置换] = 8 port5 uOPs。
  • 3 [add] = 3 port01 uOPs。

吞吐量将成为端口5的瓶颈。延迟非常糟糕,大约需要18个周期。

[明智地]使用vhadd的代码很难通过gcc向量扩展名获得,因此该代码需要特定于Intel的内在函数:

v4f64 dfoo_hadd(v4f64 sv0, v4f64 sv1, v4f64 sv2, v4f64 sv3)
{
  v4f64 hv[2];
  hv[0] = __builtin_ia32_haddpd256(sv0, sv1); //[00+01, 10+11, 02+03, 12+13]
  hv[1] = __builtin_ia32_haddpd256(sv2, sv3); //[20+21, 30+31, 22+23, 32+33]
  v4f64 fv[2];
  fv[0] = __builtin_shuffle(hv[0], hv[1], (v4u64){0, 1, 4, 5}); //[00+01, 10+11, 20+21, 30+31]
  fv[1] = __builtin_shuffle(hv[0], hv[1], (v4u64){2, 3, 6, 7}); //[02+03, 12+13, 22+23, 32+33]
  return fv[0] + fv[1]; //[00+01+02+03, 10+11+12+13, 20+21+22+23, 30+31+32+33]
}

这将生成以下程序集:

dfoo_hadd:
    vhaddpd %ymm3, %ymm2, %ymm2
    vhaddpd %ymm1, %ymm0, %ymm0
    vinsertf128 $1, %xmm2, %ymm0, %ymm1
    vperm2f128  $49, %ymm2, %ymm0, %ymm0
    vaddpd  %ymm0, %ymm1, %ymm0
    ret

根据Agner Fog的说明表,

  • vhaddpd:6个周期的延迟,每个周期的吞吐量0.5、3个uOPS port01 + 2 * port5。

总共有

  • 4 [hadd] + 2 [insert / permute] = 6 uOPs port5。
  • 3 [hadd / add] = 3 uOPs port01。

吞吐量也受端口5的限制,这比转置代码具有更高的吞吐量。延迟应约为16个周期,也比转置代码更快。

总而言之,hadd代码更快。如果要增加吞吐量,则必须将工作从port5移开。不幸的是,几乎所有的置换/插入/混洗指令都需要端口5,并且交叉通道指令(此处需要)至少具有3个周期的延迟。 几乎有用的一条有趣的指令是vblendpd,它具有3个/周期的吞吐量,1个周期的延迟,并且可以在port015上执行,但是使用它来替换permute / insert / shuffle之一将需要64-向量的128位通道的位移位,由vpsrldq/vpslldq实现,您猜到了,它需要一个port5 uOP(因此对于32位float的向量,此would帮助,因为vpsllq/vpsrlq是否需要端口[5]。这里没有免费的午餐。* gcc矢量扩展快速描述:

该代码使用gcc向量扩展,它允许对向量使用基本运算符(+-*/=><>><<等),逐个元素地操作。它们还包括一些__builtin_*函数,尤其是__builtin_shuffle(),具有3个操作数形式,其中前两个是相同类型T的两个(相同长度的N个)向量,(在逻辑上)连接到一个类型为T的双倍长度(2N)向量,第三个是整数类型(IT)的向量,其宽度和长度(N)与原始向量的类型相同。结果是与原始向量具有相同类型T和宽度N的向量,其元素由整数类型向量中的索引选择。

最初,我的回答是关于uint64_t,在此保留以供参考:

#include <stdint.h> typedef uint64_t v4u64 __attribute__((vector_size(32))); v4u64 foo(v4u64 sv0, v4u64 sv1, v4u64 sv2, v4u64 sv3) { v4u64 tv[4]; tv[0] = __builtin_shuffle(sv0, sv1, (v4u64){0,4,2,6}); tv[1] = __builtin_shuffle(sv0, sv1, (v4u64){1,5,3,7}); tv[2] = __builtin_shuffle(sv2, sv3, (v4u64){0,4,2,6}); tv[3] = __builtin_shuffle(sv2, sv3, (v4u64){1,5,3,7}); v4u64 fv[4]; fv[0] = __builtin_shuffle(tv[0], tv[2], (v4u64){0,1,4,5}); fv[1] = __builtin_shuffle(tv[0], tv[2], (v4u64){2,3,6,7}); fv[2] = __builtin_shuffle(tv[1], tv[3], (v4u64){0,1,4,5}); fv[3] = __builtin_shuffle(tv[1], tv[3], (v4u64){2,3,6,7}); return fv[0]+fv[1]+fv[2]+fv[3]; }

gcc-9.2.1在skylake-avx2上生成的翻译看起来像这样:

foo: vpunpcklqdq %ymm3, %ymm2, %ymm5 vpunpcklqdq %ymm1, %ymm0, %ymm4 vpunpckhqdq %ymm3, %ymm2, %ymm2 vpunpckhqdq %ymm1, %ymm0, %ymm0 vperm2i128 $32, %ymm2, %ymm0, %ymm3 vperm2i128 $32, %ymm5, %ymm4, %ymm1 vperm2i128 $49, %ymm2, %ymm0, %ymm0 vperm2i128 $49, %ymm5, %ymm4, %ymm4 vpaddq %ymm4, %ymm1, %ymm1 vpaddq %ymm0, %ymm3, %ymm0 vpaddq %ymm0, %ymm1, %ymm0 ret

请注意,程序集在与gcc矢量扩展名对应的行中几乎包含一行。

根据Agner Fog关于Skylake的说明表,

    [vpunpck[h/l]qdq:1个周期延迟,每个周期吞吐量1个,端口5。
  • [vperm2i128:3个周期的延迟,每周期1个吞吐量,端口5。
  • [vpaddq:1个周期的延迟,每个周期3个吞吐量,端口015。
  • 因此,转置需要10个周期(4个用于拆包,4个吞吐+ 2个等待时间用于置换)。在这三个添加项中,只有两个可以并行执行,因此将花费2个周期,总共12个周期。
© www.soinside.com 2019 - 2024. All rights reserved.