更快的方法来测试xmm / ymm寄存器是否为零?

时间:2017-02-18 16:13:22

标签: assembly optimization x86 avx micro-optimization

幸运的是PTEST 不会影响进位标志,但只会设置(相当笨拙的)ZF。也会影响CF和ZF。

我想出了以下序列来测试大量值,但我对运行时间不佳感到不满。

              Latency / rThoughput
setup:
  xor eax,eax       ; na
  vpxor xmm0,xmm0   ; na       ;mask to use for the nand operation of ptest
work:
  vptest xmm4,xmm0  ; 3   1    ;is xmm4 alive?
  adc eax,eax       ; 1   1    ;move first bit into eax
  vptest xmm5,xmm0  ; 3   1    ;is N alive?
  adc eax,eax       ; 1   1    ;move consecutive bits into eax 

我想在eax中得到所有非零寄存器的位图(显然我可以在多个寄存器中组合多个位图)。

因此,每个测试的延迟为3 + 1 = 4个周期 其中一些可以在eaxecx等之间交替并行运行 但它仍然很慢 有更快的方法吗?

我需要连续测试8个xmm / ymm寄存器。一个字节位图中每个寄存器1位。

1 个答案:

答案 0 :(得分:6)

实际上,现有方法不是“非常慢”,而是合理的。

当然每个测试的延迟时间为4个周期 1 ,但如果您希望将结果存入通用寄存器,则通常需要支付3个周期无论如何,该移动的延迟(例如,movmskb也具有3的延迟)。在任何情况下,您都想测试8个寄存器,而不是简单地添加延迟,因为每个寄存器大多是独立的,因此uop计数和端口使用可能最终会使测试单个寄存器的延迟变得更加重要延迟与其他工作重叠。

在英特尔硬件上可能会快一点的方法是使用连续的PCMPEQ指令,测试几个向量,然后将结果折叠在一起(例如,如果你使用PCMPEQQ,你实际上有4个四字结果并且需要将它们折叠成1)。您可以在PCMPEQ之前或之后折叠,但这有助于更多地了解您希望结果如何/在哪里提出更好的结果。以下是8个寄存器的未经测试的草图,xmm1-8假设为xmm0为零,xmm14pblendvb掩码,用于选择最后一条指令中使用的备用字节。

# test the 2 qwords in each vector against zero
vpcmpeqq xmm11, xmm1, xmm0
vpcmpeqq xmm12, xmm3, xmm0
vpcmpeqq xmm13, xmm5, xmm0
vpcmpeqq xmm14, xmm7, xmm0

# blend the results down into xmm10   word origin
vpblendw xmm10, xmm11, xmm12, 0xAA   # 3131 3131
vpblendw xmm13, xmm13, xmm14, 0xAA   # 7575 7575
vpblendw xmm10, xmm10, xmm13, 0xCC   # 7531 7531

# test the 2 qwords in each vector against zero
vpcmpeqq xmm11, xmm2, xmm0
vpcmpeqq xmm12, xmm4, xmm0
vpcmpeqq xmm13, xmm6, xmm0
vpcmpeqq xmm14, xmm8, xmm0

# blend the results down into xmm11   word origin
vpblendw xmm11, xmm11, xmm12, 0xAA   # 4242 4242
vpblendw xmm13, xmm13, xmm14, 0xAA   # 8686 8686
vpblendw xmm11, xmm11, xmm13, 0xCC   # 8642 8642

# blend xmm10 and xmm11 together int xmm100, byte-wise
#         origin bytes
# xmm10 77553311 77553311
# xmm11 88664422 88664422
# res   87654321 87654321 
vpblendvb xmm10, xmm10, xmm11, xmm15

# move the mask bits into eax
vpmovmskb eax, xmm10
and al, ah

直觉是你在每个QWORD中测试每个xmm对零,为8个寄存器提供16个结果,然后将结果混合到xmm10中,最后得到一个每个字节的结果按顺序排列(所有低QWORD结果之前的所有高QWORD结果)。然后将这些16字节掩码作为16位移动到eax movmskb,最后将QWORD内的每个寄存器的高eax位组合起来。

对我来说,总共有16个uop,对于8个寄存器,每个寄存器大约有2个uop。总延迟是合理的,因为它主要是“减少”类型的并行树。一个限制因素是6 vpblendw个操作,这些操作都只适用于现代英特尔的5端口。最好用VPBLENDD替换其中4个,这是p015任何一个“祝福”的混合。这应该是直截了当的。

所有操作都简单快捷。最后的and al, ah是部分寄存器写入,但是如果你mov进入eax之后,也许没有惩罚。如果这是一个问题,你也可以通过几种不同的方式完成最后一行......

这种方法也可以自然地扩展到ymm个寄存器,最后在eax中折叠略有不同。

修改

稍微快一点的结尾使用打包转换来避免两个昂贵的指令:

;combine bytes of xmm10 and xmm11 together into xmm10, byte wise
; xmm10 77553311 77553311
; xmm11 88664422 88664422   before shift
; xmm10 07050301 07050301
; xmm11 80604020 80604020   after shift
;result 87654321 87654321   combined
vpsrlw xmm10,xmm10,8
vpsllw xmm11,xmm11,8
vpor xmm10,xmm10,xmm11

;combine the low and high dqword to make sure both are zero. 
vpsrldq xmm12,xmm10,64
vpand xmm10,xmm12
vpmovmskb eax,xmm10

通过避免2个周期vpblendvbor al,ah的部分写入惩罚来节省2个周期,如果不需要使用,还可以修复对慢vpmovmskb的依赖性。该指示的结果立即生效。

1 实际上似乎仅在Skylake上PTEST有三个周期的延迟,之前它似乎是2.我也不确定1个周期您为rcl eax, 1列出的延迟:根据Agner的说法,现代英特尔似乎是3 uops和2个周期的延迟/接收吞吐量。