assembly x86-64指令与直到零?

xsuvu9jc  于 2023-04-06  发布在  其他
关注(0)|答案(1)|浏览(163)

我知道有一个重复的指令(例如repnz)。
我有一个情况,我有一个(8位)数组[7, 2, 3, 4, ..., 7, 0, 0, 0 ...](我在末尾有64个零字节)。我想加载第一个值(7)并与其余的值进行AND,直到有一个零:

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

所以我想在这里停下来,并有一个指针或索引3(数组[3] == 4)。我还需要结果变为零之前的值。
有没有一个rep指令或者SIMD指令我可以用来找到它?有没有什么聪明的C代码我可以写?目前我使用一个简单的while循环,我以导致结果变为零的索引和'previousResult'结束,这样我就可以使用结果为零之前的值(在上面的例子中为2)

sy5wg1nm

sy5wg1nm1#

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

_mm_and_si128替换_mm_add_epi8。在前缀AND-scan中用pcmpeqb查找第一个零,然后是通常的pmovmskb/bsf(或tzcnt),如果前16个字节中有一个零。
相关问答:

AVX-512有一些有趣的指令。vpternlogd可以执行3输入AND,但只能垂直执行,不能在一个向量内执行,因此您必须进行 Shuffle 设置。也许在前缀AND扫描中会有用。
其他AVX-512 SIMD指令可能没有用。vp2intersectd(仅限https://felixcloutier.com/x86/vp2intersectd:vp2intersectq- Tiger Lake)查找元素之间的两两相等,而不是按位相交。同样,vpconflictd只查找整数的精确匹配。像VGF2P8AFFINEQB这样的GFNI指令可以做一些整洁的事情,但是如果正确的常量可以让你跨字节进行按位AND,IDK可能不会。
我没有确切的数字,但长度1-5是非常受欢迎的,8和12是不受欢迎的,但并不罕见。我怀疑做一个SIMD加载+一些shuffles +得到索引0可能会比一个1字节的循环,当数据的长度是5或更少,这是所有的时间。所以我想我卡住了,或者必须找到一种新的方式来表示数据。
是的,对于短长度,SIMD可能会更慢,只要分支在短版本中被正确预测。也许剥离前3或4次迭代并使用一些cmov可以减少分支预测错误。(对于吞吐量,如果这个线程与超线程共享一个核心,分支未命中并不是那么糟糕,尽管它在被检测到之前仍然浪费了一些前端带宽。)
也许一些整数SWAR可以创建一些ILP(指令级并行性),比如首先用test al, dl/jnz检查x & a[1],同时一次在64位上执行a & (a>>8)?然后是and al, dldl holds a[1]&a[2]的东西。展开两个,这样每次迭代都不执行一个分支可能是好的。
除了编译器可以做的之外,通常不值得手工优化asm,但是x86部分寄存器在较宽移位的低字节上执行字节大小的AND可能很难让编译器发出。只要我们使用一个值零扩展到unsigneduint64_t,AND在较高的字节中将为零,所以最坏的情况是,愚蠢的编译器将浪费一些雷克斯前缀,而它本可以使用32位操作数大小。(在C中,memcpyuint64_t用于未对齐的别名安全加载)。
为了好玩,在https://uica.uops.info/提醒我在Intel CPU上移位与分支竞争端口0和6之前,我尝试编写了一些asm。因此,对于关键路径延迟来说,资源冲突与更长的依赖链一样是一个严重的问题,并且很坚韧让它在每次迭代中执行8个周期以上,或者在Ice Lake上检查每个字节约1个周期。这是一个简单的内存源and al, [rdi]/jz
左移eax/rax以对齐x(RAX中的一个字节,不一定是最低的)(a[i+1..8])和RSI(a[i+1..8] & a[i+2..9])用2个班次交换1个班次(在末尾加上1个额外的移位以使RAX中的字节向下回到底部。)它还增加了关键路径延迟,与移位独立负载结果相比,移位和AND两者作为RAX的循环承载依赖性链的一部分。
也许我们可以用64位AND并行做其他工作,比如对最后2对字节进行水平AND?但是将0xFF放入RAX,然后提取较高的字节作为早期输出将需要额外的指令,可以跳过最后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 says Ice Lake / Rocket Lake可以以每次迭代7个周期(检查8个字节)的速度运行它,仅仅比每个周期一个AND快一点。
在冰湖上是相当挑剔的,至少在uiCA对管道的模拟中是这样(假设完美的分支预测等)在某些点使用一些shl rax, 16而不是shr rcx,16/shr rsi,16的版本预计在Ice Lake上运行约8 c/ iter。(使用雷克斯前缀使两条指令更长),避免了Skylake上的JCC勘误表,在ICL上减慢到8.85c,但在Skylake上加速到约10 c。(uiCA,清理未调整以从RAX中获取正确的字节)。

但这种做法很可能不好;也许更好的工作在2或4字节的块,所以更多的加载和更少的移位。也许甚至2字节加载到一个寄存器,然后test al,cl/and cl, ch/and al, cl或其他东西。阅读CH在Intel上有一个周期额外的延迟,但不消耗吞吐量。

相关问题