我知道有一条指令会重复(例如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可能很难让编译器发出。但实际上我们并不需要这样;只要我们使用的是扩展到unsigned
或uint64_t
中的值0,在较高的字节中AND将为零,所以在最坏的情况下,愚蠢的编译器会在可能使用32位操作数大小的地方浪费一些REX前缀。(在C中,memcpy
到uint64_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在英特尔上有一个额外的周期延迟,但不需要吞吐量。