已经有一个关于horizontal sums using AVX512的问题了.我试着做一些类似的事情,但在求和之后,我想将结果广播给__m512d变量中的所有8个元素.到目前为止,我已经try 了:

  1. 使用英特尔提供的宏:
double sum = _mm512_reduce_add_pd( mvx );
sumx = _mm512_set1_pd( sum );
  1. 使用随机/置换,尽量避免车道交叉:
sumx = mvx;

mvx = _mm512_shuffle_pd(mvx, mvx, 0b01010101);
sumx = _mm512_add_pd(mvx, sumx);

mvx = _mm512_permutex_pd(mvx, _MM_PERM_ABCD);
sumx = _mm512_add_pd(mvx, sumx);

mvx = _mm512_shuffle_pd(mvx, mvx, 0b01010101);
sumx = _mm512_add_pd(mvx, sumx);

mvx = _mm512_shuffle_f64x2(mvx,mvx, _MM_SHUFFLE(1,0,3,2));
sumx = _mm512_add_pd(mvx, sumx);

mvx = _mm512_shuffle_pd(mvx, mvx, 0b01010101);
sumx = _mm512_add_pd(mvx, sumx);

mvx = _mm512_permutex_pd(mvx, _MM_PERM_ABCD);
sumx = _mm512_add_pd(mvx, sumx);

mvx = _mm512_shuffle_pd(mvx, mvx, 0b01010101);
sumx = _mm512_add_pd(mvx, sumx);

  1. 使用@PeterCordes的提示,将添加/洗牌减少到3:
sumx = mvx;

mvx = _mm512_shuffle_pd(mvx, mvx, 0b01010101);
sumx = _mm512_add_pd(mvx, sumx);

mvx = _mm512_permutex_pd(sumx, _MM_PERM_ABCD);
sumx = _mm512_add_pd(mvx, sumx);

mvx = _mm512_shuffle_f64x2(sumx,sumx, _MM_SHUFFLE(1,0,3,2));
sumx = _mm512_add_pd(mvx, sumx);

在每种情况下,mvx__m512d输入,sumx__m512d输出.

我正在使用英特尔编译器在英特尔Skylake CPU上进行基准测试:

  • 版本1:2.17s
  • 版本2:2.31s
  • 版本3:1.96s

这是我能做的最好的事情了吗?或者你有没有其他方法来优化这个操作?

推荐答案

通常,最好的方法是对swap个一半进行计算,而不是缩小范围,以便在两个一半中计算出相同的总和.(特别是如果您不关心Zen 4或假设的future CPU,在这些CPU中,缩小到256位具有吞吐量优势.)处理__m512d中的2^3=8双精度应该只需要3个洗牌/添加步骤,其中一个是通道内添加对.

Your second version is correctly doing that, and looks optimal on current CPUs (Intel and Zen 4.)

像您所做的那样,首先进行低延迟的通道内混洗对无序执行是有好处的,让更多的uop执行并提前几个周期停用,以便在调度器和Rob中为新的可能独立的工作更快腾出空间.

在当前的Intel CPU上,所有32位或更宽粒度的512位交叉车道混洗都具有相同的性能:端口5具有3c延迟的1个uop.对于具有1c延迟的端口5,通道内512比特混洗是1微微操作.

在禅宗4上,vshufpdvpermilpd都有相同的时间,vpermpd/vshuff64x2/vshuff32x4/valignq也是如此.(https://uops.info/)所有这些混洗的控件都有立即操作数,因此编译器不必加载向量常量.


Any tweaks would just be based on guess-work about what might be faster on possible future CPUs,比如future 支持AVX-512的英特尔E-core,或者他们在future 的CPU中用作E-core的精简AMD Zen 4,如果他们完全改变执行单元而不仅仅是缓存的话.或者future 可能有空间容纳多个512位混洗单元的大型内核.这段代码在所有6个操作中都具有序列依赖关系,但是能够在更多端口上运行可能会让无序执行更好地同时运行这段代码和一些独立的周边代码,或者运行另一个逻辑核心.

从历史上看,使用可用的最宽粒度的洗牌一直是最好的.例如,在Zen 4上,vextractf64x4 ymm, zmm, immvextractf64x2 xmm, zmm, imm快,所以更喜欢前者来提取第三个128位的块,即使你不介意带来大量的垃圾.更少的较大块意味着更少的可能安排,更短的多路复用链,因此可能具有更低的延迟或在更多的执行单元上运行.但是没有vshuff64x4个,只有vshuff64x2个128位的块,所以这是我们交换256位一半的唯一好 Select .

如果专门针对Zen 4进行调整,而不考虑英特尔,则vextractf64x4+vinsertf64x4的总延迟比vshuff64x2 zmm低,尽管前端的成本是2次微调而不是1次.除了插入/提取256位半部分之外,Zen 4上的512位置乱占用了它们的执行单元2个周期(1/时钟的实际吞吐量是两个端口之一的一个uop的预期吞吐量的一半,就像Zen 4如何处理其他不需要在两个半部分之间移动数据的512位uop一样).

For the middle shuffle, swapping 128-bit halves,在vshuff64x2 z,z,z,imm8vpermpd z,z,imm8之间 Select .两者在当前的CPU(包括Zen 4)上都运行相同.我们可能会基于更大的粒度(以128位块而不是64位块来移动数据)来 Select vshuff64x2,但还有另一个因素需要考虑:vpermpd z,z,imm8在每一半中执行独立的256位混洗,因此对256位执行单元的分解微不足道.(与矢量控制版本不同,该版本在整个矢量中有8个3位索引可供 Select .)

Zen 4的混洗执行单元本质上是512位宽,因此它们只会降低512位操作的吞吐量和更高的延迟.但future 可能搭载AVX-512的英特尔E核可能不会做到这一点,运行速度可能会像Zen 1的vperm2f128 y,y,y,imm8(8uop,尽管这看起来有点过高)或vpermps y,y,y(3uop)一样慢.Alder Lake中的英特尔E-core设法将这些作为2个uop处理,因此,假设支持AVX-512的具有256位执行单元的E-core也可以将vshuff64x2 z,z,z,imm8作为2个uop处理.

vshuff64x2 z,z,z,imm8需要vperm2f128 ymm, ymm, ymm, imm4比特的输入(因为它可以接受两个不同的输入向量),但前2个输出通道是从第一个输入 Select 的(因此只有4个可能的输入比特必须通过多路复用器路由到每个输出比特),来自第二个源的第二两个输出通道也是如此.因此,它可以分解为两个单独的512位输入/256位输出混洗,类似于256位valignq ymm,ymm,ymm, imm或类似vperm2f128 ymm, ymm, ymm, imm,但每个输出通道能够 Select 四个中的任何一个.(valignq zmm实际上是最后一次洗牌的另一种可能性,但不太可能便宜.)

因此,vshuff64x2 zmm实际上是以一种方式设计的,这可能会使使用比您想象的更窄的执行单元来实现它的成本更低,比valignqvpermt2ps或其他两个输入混洗要容易得多,在其他两个输入混洗中,每个输出可以从两个512位输入的任何位置挑选.


人们可能会猜测,使用相同输入的a one-input shuffle _mm512_permute_pd(mvx, 0b01'01'01'01);(又名vpermilpd z,z, imm)might be more efficient on some future CPUvshufpd z,z,z, imm高出两倍.这在《骑士登陆》(Xeon Phi)上是真的,但我想你并不关心这一点,因为它已经停产几年了,我也没有看过它的时间是vpermpdvshuff64x2.

但在Ice Lake上,更常见的vshufpd y,y,y,i具有2/时钟吞吐量,而不是1/时钟vpermilpd y,y,i1.那么,谁能猜到future 配备AVX-512的E核或future 可能有空间容纳多个512位洗牌单元的大内核上,哪些洗牌会更快.

摘要:

  • 第一次洗牌vshufpd就够了.即使向量从内存中开始,您也不需要内存源vpermilpd,因为您需要将向量的另一个副本作为vaddpd的输入.可以在future 的E-core上以更低的成本处理其中一个.这是一个通道内洗牌,所以它分解成多个更窄的洗牌,对于E-core来说是微不足道的.

  • 对于中间混洗(交换128位对)来说,vpermpd-Immediate是一个很好的 Select ;future 的E-core很可能可以有效地处理它(作为两个独立的256位半部分).不过,vshuff64x2可以分解为两个单独的512位输入/256位输出混洗,因此也不算差.

    带有向量控制操作数的vpermpd不容易分解,但它是一个不同的操作码,因此希望即使向量控制版本速度较慢,直接控制版本仍然会很便宜.不知何故,Alder Lake E-core确实成功地将vpermps ymm作为2个uop运行.

  • vshuff64x2valignq对于在Intel CPU上交换256位的一半同样好,并且在Zen 4上彼此相等.对于E-core来说,vshuff64x2显然更容易有效地实现:两者具有相同的输入量(vshuff64x24位),但对于任何给定的输出位,vshuff64x2的可能来源要少得多(4比16,并且如果两个来源不是相同的寄存器,则对哪个来源供给哪个输出的限制更多).此外,这可能是一种更常用的洗牌,因此建筑师更有可能使用晶体管来使其不太慢.

    vextractf64x4+vinsertf64x4在Zen 4上的延迟会更低,这可能会影响也可能不会影响到周围的代码.但vshuff64x2 zmm仍然是Zen 4上的Single-uop,只有4个周期的延迟,就像其他512位的穿越车道洗牌一样.假设带有AVX-512的较小内核可能会运行2个或更多.


Footnote 1:IDK为什么Ice Lake/Alder Lake不能使用寄存器源和立即控制将vpermilpd解码为读取相同输入两次的vshufpd微操作,因为在这种情况下,相同的立即位将产生相同的置乱.这似乎是一个遗漏的优化,尽管它可能会在解码器中的某个地方产生一个uop,其中内存源版本的1个输入与寄存器源版本的2个输入产生uop.因此,改为更改Shuffle执行单元以在这种情况下复制一个输入,作为让端口1处理vpermilpd个uop的一种方式,从而使得以这种方式处理内存源并不特别.以不得不在混洗单元的端口1输入上处理更多不同的控制输入为代价?

在Ice Lake/Alder Lake上,当没有512位uop运行时,端口1执行单元可以处理一些但不是全部128位和256位混洗.它可能只是512位混洗执行单元的一半,512位混洗执行单元通常可以从端口5访问.(同样的方式,它们处理端口0或1上的256位FP数学指令,但当端口1关闭时,它作为单个512位FMA单元工作.)因此,当混洗单元的通道处于端口5的vpermilpd zmm, zmm, imm8的上半部分时,它可以处理vpermilpd.因此,当通过端口1访问时,似乎只需要最少的额外逻辑就能做到这一点.(vpermilpd zmmvshufpd zmm以彼此相同的方式使用其立即数的高4位,并且与低4位对低半部起作用相同.每条128位通道都有2位控制输入.)

我想知道是否有意确保vpermilpd/ps不能从FP数学运算中窃取周期(256位的端口0和1).这可能是有意义的,甚至可能对调整P01吞吐量与Shuffle吞吐量之间的瓶颈的循环很有用:他们可以使用vshufpd y, same,same, i让它在端口1或5上运行,或者只在较小的机器代码大小(2字节VEX)上运行.或vpermilpd y, ymm/mem, i将其限制为端口5,如果vshufpd不需要3字节的VEX,则代价是机器代码大小的额外字节.(或者,如果它正在混洗内存源,则为整个单独的指令.但像许多具有立即操作数的指令一样,Intel CPU不能微融合Load+ALU uop,因此发布带宽的成本是相同的.)

这似乎不太可能.也许他们只是分析了现有的代码,发现shufpd/vshufpd更常见,因此也更重要;这并不奇怪,因为shufpd是SSE2,而vpermilpd直到AVX1才存在.因此,这一因素可能是影响与 Select YMM Shuffles相关的设计的原因,尽管vshufpd ymmvpermilpd都是AVX1的新功能.

但对future 的猜测是,Alder Lake的英特尔Gracemont E-core性能相同,分别为vpermilpd ymm, ymm, i8vshufpd ymm, ymm, ymm, i8.

C++相关问答推荐

C限制限定符是否可以通过指针传递?

GCC:try 使用—WError或—pedantic using pragmas

手动矢量化性能差异较大

为什么内核使用扩展到前后相同的宏定义?

二进制计算器与gmp

我怎么才能用GCC编译一个c库,让它包含另一个库呢?

在循环中复制与删除相同条件代码的性能

用gcc-msse 2编译的C程序包含AVX 1指令

在句子中转换单词的问题

C代码在字符串中删除不区分大小写的子字符串的问题

关于scanf()和空格的问题

获取前2个连续1比特的索引的有效方法

C语言中神秘的(我认为)缓冲区溢出

用C++构建和使用DLL的困惑

问题:C#Define上的初始值设定项元素不是常量

Linux/C:带有子进程的进程在添加waitid后都挂起

如何找出C中分配在堆上的数组的大小?

如何为avr atmega32微控制器构建C代码,通过光电二极管捕获光强度并通过串行通信传输数据

添加/删除链表中的第一个元素

多行表达式:C 编译器如何处理换行符?