x86-64指令与,直到为零



我知道有一条指令会重复(例如repnz)
我有一个[7, 2, 3, 4, ..., 7, 0, 0, 0 ...]的(8位)数组(末尾有64个零字节)。我想加载第一个值(7),并将其与其他值进行and运算,直到出现零:

7 & 2 == 2
2 & 3 == 2
2 & 4 == 0

所以我想在这里停下来,用一个指针或索引3(array[3]==4)。我还需要结果为零之前的值。

我可以使用rep指令或SIMD指令来找到它吗?有什么聪明的C代码我能写吗?目前,我使用了一个简单的while循环,并以导致结果变为零的索引和"previousResult"结束,这样我就可以在值变为零之前立即使用它(上例中为2)

如果长时间运行很常见,您可以使用与前缀和相同的模式进行一些移位和and,只是使用and而不是加法。与标量循环相比,这有额外的设置和清理开销,但可以很好地扩展较大的大小,并避免分支预测失误。

_mm_add_epi8替换为_mm_and_si128。在前缀AND扫描中查找第一个零,用pcmpeqb对零向量进行扫描,如果前16个字节中有零,则查找通常的pmovmskb/bsf(或tzcnt)。

相关问答;As:

  • 英特尔cpu上的SIMD前缀和
  • 如何初始化范围从0到N的SIMD矢量?-相似的混洗模式
  • 在__m256i矢量上水平累积一个连续总数(前缀和)

AVX-512有一些有趣的指令。vpternlogd可以进行3输入AND,但只能在垂直方向上进行,而不能在一个向量内进行,因此必须进行混洗才能进行设置。也许这在前缀AND扫描中很有用。

其他AVX-512 SIMD指令可能没有用处。vp2intersectd(https://felixcloutier.com/x86/vp2intersectd:vp2intersectq-仅限Tiger Lake)寻找元素之间的成对相等,而不是逐位相交。类似地,vpconflictd只是寻找整整数的精确匹配。像VGF2P8AFFINEQB这样的GFNI指令可以做一些巧妙的事情,但如果正确的常量,IDK可以让你在字节之间进行逐位AND。可能不会。


我没有确切的数字,但长度1-5非常受欢迎,8和12不受欢迎,但并不罕见。我怀疑,当数据长度为5或更短时,进行SIMD加载+几次洗牌+获得索引0可能比1字节循环更麻烦,这是所有时间。所以我想我被卡住了,或者必须找到一种新的方法来表示数据。

是的,对于短长度,只要在短版本中正确预测分支,SIMD可能会更慢。也许剥离前3或4次迭代并使用一些cmov可以减轻分支预测失误。(对于吞吐量,如果这个线程与超线程共享一个核心,那么分支未命中也没那么糟糕,尽管在被检测到之前它仍然会浪费一些前端带宽。)

也许某个整数SWAR可以创建一些ILP(指令级并行性),比如首先用test al, dl/jnz检查x & a[1],同时对64位执行a & (a>>8)?然后是CCD_ 17或CCD_。通过两个分支展开,这样就不会在每次迭代中使用一个分支,这可能是很好的。

通常不值得手动优化asm,超出编译器可以做的范围,但x86部分寄存器在较宽移位的低字节上进行字节大小的AND可能很难让编译器发出。但实际上我们并不需要这样;只要我们使用的是扩展到unsigneduint64_t中的值0,在较高的字节中AND将为零,所以在最坏的情况下,愚蠢的编译器会在可能使用32位操作数大小的地方浪费一些REX前缀。(在C中,memcpyuint64_t用于未对齐的混叠安全负载)。

只是为了好玩,我以前试过写一些讽刺https://uica.uops.info/提醒我,移位与分支竞争英特尔CPU上的端口0和6。因此,对于关键路径延迟来说,资源冲突和较长的依赖链一样是一个严重的问题,很难让它在每次迭代中执行超过8个周期,或者在Ice Lake上检查的每字节大约1个周期,这大约是从简单的内存源and al, [rdi]/jz中得到的

左移eax/rax使x(RAX中的一个字节,不一定是最低的)与RCX(a[i+1..8])和RSI(a[i+1..8] & a[i+2..9])的不同位置对齐,用2个移位换1个(加上末端的1个额外移位,使RAX中字节回到底部。)它还增加了关键路径延迟,移位和and都是RAX的循环依赖链的一部分,与移动独立负载结果相比。

也许我们可以用64位AND并行进行其他工作,比如对最后2对字节进行水平AND?但是,在RAX中获得0xFF,然后提取较高的字节作为早期输出,需要额外的指令,才能跳过最后4个字节?

prefix_AND_scan_zero:   ; const uint8_t a[]
movzx eax, byte [rdi]     ; x = a[0]
.outer_loop:
mov   rcx, [rdi+1]
mov   rsi, [rdi+2]        ; rorx  rsi, rcx, 8   ; last byte would be less useful.
and   rsi, rcx            ; a1 &= a2,  a3 &= a4, etc.
;.inner_loop:   ; fully unrolled
test  eax, ecx           ; x & a[i+1]
jz   .first_element      ; RDI+1 is the pointer, AL holds the value
mov   edx, eax           ; save old x before potentially zeroing it
and   eax, esi           ; x &= (a[i+1] & a[i+2])
jz   .second_element
shr  rcx, 16            ; shift both the other things instead of rax,
shr  rsi, 16            ;   keeping the critical path shorter
test  eax, ecx          ; x & a[i+1]
jz   .third_element      ; RDI+3 is the pointer, AL holds the value
mov   edx, eax          ; save old x before potentially zeroing it
and   eax, esi          ; x &= (a[i+1] & a[i+2])
jz   .fourth_element
add  rdi, 4
shr  rcx, 16            ; a[i+1..8] >> 32
shr  rsi, 16
;   shl  eax, 16            ; x now lives in the 3rd byte of RAX
; saves front-end bandwidth but lengthens critical path and requires separate branch targets to sort out where the value is.
test  eax, ecx          ; x & a[i+1]
jz   .first_element      ; RDI+1 is the pointer, AL holds the value
mov   edx, eax          ; save old x before potentially zeroing it
and   eax, esi          ; x &= (a[i+1] & a[i+2])
jz   .second_element
shr  ecx, 16            ; a[i+1..8] >> 48
shr  esi, 16
test  eax, ecx          ; x & a[i+1]
jz   .third_element      ; RDI+1 is the pointer, AL holds the value
mov   edx, eax          ; save old x before potentially zeroing it
and   eax, esi          ; x &= (a[i+1] & a[i+2])
jz   .fourth_element
add   rdi, 4
;   shr   eax, 16     ; x back to the bottom of RAX if we were shifting it
jmp  .outer_loop        ; trailing zeros in the array will make the loop exit without needing a counter
.fourth_element:
add  rdi, 4
.second_element:
;; RDI+2 is the pointer, value is DL & [rdi+1] (the discarded result of the last TEST)
;; recomputing it here keeps the loop's critical path latency short
movzx  eax, byte [rdi+1]
and    eax, edx
add    rdi, 2
ret
.third_element:
add  rdi, 4
.first_element:     ; RDI+1 is the pointer, AL holds the value
inc  rdi
ret

(未经测试,可能不是最佳的,尤其是在英特尔;太多的班次和分支机构竞争端口0和6。)

只要输入数组中有一个保证为零的条件,就不需要外循环条件;我们肯定会停在那里,如果不是更早的话,所以不需要循环计数器,只需要更新指针来跟踪进度。

uiCA表示,Ice Lake/Rocket Lake可以以每次迭代7个周期(检查了8个字节)的速度运行,只比每个周期一个AND快一点。在Skylake上,它遇到了JCC的勘误表。

至少在uiCA对管道的模拟中(假设完美的分支预测等),在Ice Lake上这是非常挑剔的。在某些点使用一些shl rax, 16而不是shr rcx,16/shr rsi,16的版本预计在Ice Lake上以大约8c/iter的速度运行。通过一些调整(使两条带有REX前缀的指令变长)来避免Skylake上的JCC错误,它在ICL上降到8.85c,但在Skylake上将速度提高到约10c。(uiCA,未调整清理以从RAX中获得正确的字节)。

但这种方法可能并不好;也许最好是以2或4字节的块进行工作,这样可以增加负载,减少移位。甚至可能将2字节加载到寄存器中,然后是test al,cl/and cl, ch/and al, cl或其他什么。读取CH在英特尔上有一个额外的周期延迟,但不需要吞吐量。

最新更新