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

Faster way to test if xmm/ymm register is zero?

幸运的是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 位。

而不是 "quite slow" 你现有的方法实际上是合理的。

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

一种在 Intel 硬件上可能会更快一点的方法是使用连续的 PCMPEQ 指令来测试多个向量,然后将结果折叠在一起(例如,如果您使用 PCMPEQQ,您实际上有 4四字结果,需要将它们 and-fold 化为 1)。您可以在 PCMPEQ 之前或之后弃牌,但了解更多有关 how/where 的信息会有助于您得出更好的结果。这是 8 个寄存器的未经测试的草图,xmm1-8xmm0 假定为零,xmm14 是最后一条指令中使用的 select 备用字节的 pblendvb 掩码。

# 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

直觉是你测试每个 xmm 中的每个 QWORD 是否为零,为 8 个寄存器给出 16 个结果,然后你将结果混合到 xmm10 中结束每个字节一个结果,按顺序(所有 high-QWORD 结果在所有 low-QWORD 结果之前)。然后将这些 16 字节掩码作为 16 位移动到 eaxmovmskb 中,最后将 eax.[=39= 中每个寄存器的高位和低位 QWORD 组合起来]

在我看来,对于 8 个寄存器,总共 16 微指令,因此每个寄存器大约 2 微指令。总延迟是合理的,因为它主要是 "reduce" 类型的并行树。一个限制因素是 6 vpblendw 操作,它们都只到现代英特尔的端口 5。最好用 VPBLENDD 替换其中的 4 个 VPBLENDD,这是一个 "blessed" 混合到 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有3个周期的延迟,之前好像是2个。我也不是确定您为 rcl eax, 1 列出的 1 个周期延迟:根据 Agner 的说法,现代英特尔似乎是 3 微指令和 2 个周期 latency/recip 吞吐量。