我正在尝试优化一些代码,并且我处于4个向量__m256d
的状态,我想将每个向量的总和存储在另一个__m256d
中。所以基本上是result = [sum(a), sum(b), sum(c), sum(d)]
。我知道有一种方法可以使用2个hadd进行混合和置换,但是我意识到hadd太昂贵了。
因此,我想知道是否有一个内部函数可以更快地执行此操作。
让编译器为您优化此设置。使用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。总共有
吞吐量将成为端口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。总共有
吞吐量也受端口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。