我一直在尝试进一步优化这个展开的循环:
some_loop:
UNROLL_CNT = 256 ; best for Core i9-9900k at least
UNROLL_I = 0
repeat UNROLL_CNT
;
; Block 1
;
movzx r9, byte ptr[r10] ; tmp = s[i]
add r11b, r9b ; j += s[i]
movzx rax, byte ptr[r11] ; s[i] = s[j]
mov byte ptr[r10], al ;
mov byte ptr[r11], r9b ; s[j] = tmp
add r9b, al ; tmp = s[i] + s[j]
inc r10b ; i++
movzx rax, byte ptr[rsp + r9] ; rax = s[tmp]
xor byte ptr [rcx+UNROLL_I], al ; *data ^= rax => *data ^= s[s[i] + s[j]]
UNROLL_I = UNROLL_I + 1
endm
..我想我应该尝试将 2 个迭代交错在一起(使用一组额外的寄存器),但在我到达那里之前,令我惊讶的是,仅通过使用新的寄存器组,性能就下降了一半(? ):
some_loop:
UNROLL_CNT = 256
UNROLL_I = 0
repeat UNROLL_CNT/2
;
; Block 1
;
movzx r9, byte ptr[r10] ; tmp = s[i]
add r11b, r9b ; j += s[i]
movzx rax, byte ptr[r11] ; s[i] = s[j]
mov byte ptr[r10], al ;
mov byte ptr[r11], r9b ; s[j] = tmp
add r9b, al ; tmp = s[i] + s[j]
add r10b, 2 ; i+=2 (for next iter.)
movzx rax, byte ptr[rsp + r9] ; al= s[tmp]
xor byte ptr [rcx+UNROLL_I], al ; *data ^= s[s[i] + s[j]]
UNROLL_I = UNROLL_I + 1
;
; Block 2
; same as above with r9->r12, r10->r13, r11->r14, rax->r15
;
movzx r12, byte ptr[r13] ; tmp = s[i]
add r14b, r12b ; j += s[i]
movzx r15, byte ptr[r14] ; s[i] = s[j]
mov byte ptr[r13], r15b ;
mov byte ptr[r14], r12b ; s[j] = tmp
add r12b, r15b ; tmp = s[i] + s[j]
add r13b, 2 ; i+=2 (for next iter.)
movzx r15, byte ptr[rsp + r12] ; r15b = s[tmp]
xor byte ptr [rcx+UNROLL_I], r15b ; *data ^= s[s[i] + s[j]]
UNROLL_I = UNROLL_I + 1
endm
因此,第一个列表重复 256 次 x 块 1,第二个列表重复 128 次 x(块 1,块 2)。正如您所看到的,除了使用的寄存器以及变成
inc r10b
的 add r10b, 2
指令之外,指令实际上是相同的。我预计,这将使清单 1 在性能方面等同于清单 2。
我正在寻找有关特定 GP 寄存器的任何参考,这些寄存器比 x86-64 上的其他寄存器更快,但似乎没有任何参考,这让我认为这不是寄存器问题,而是一些缓存/并行化问题(尽管每个块上的指令都非常紧密地相互依赖;不确定它将如何执行并行化)。
当将 Block 2 引入图片中时,您是否知道什么可能会导致性能极端下降?
如果您对如何进一步优化 Block 1 本身有任何其他见解,那也非常受欢迎,因为这是我的最终目标,而不是交错的东西(我什至不确定它会提高整体性能) .
我在第二个列表中尝试过的事情:
UNROLL_CNT
:性能提升至 UNROLL_CNT = 32
,此时仍比单块列表慢 1.5 倍左右。add r10b/r13b, 2
替换为 inc r10b/r13b
:正如预期的那样,没有太大区别。顺便说一句,如果您想知道问题是否与循环条件有关,这里是:
add rcx, UNROLL_CNT ; data += UNROLL_CNT
dec rbx ; if --num_unrolled_loops
jnz some_loop ; continue
正如你所看到的,它在 rbx 上,在展开的循环体上根本没有使用它。
尝试这个代码,我还没有测试过它,但是如果你将它分成每个循环的 4 部分,因为 256 可以被 4 整除,这应该会更快。
some_loop:
UNROLL_CNT = 256
UNROLL_I = 0
repeat UNROLL_CNT/4 ; Unroll the loop further
;
; Block 1
;
movzx r8, byte ptr [r10] ; tmp = s[i]
add r11b, r8b ; j += s[i]
movzx rax, byte ptr [r11] ; s[i] = s[j]
mov byte ptr [r10], al ;
mov byte ptr [r11], r8b ; s[j] = tmp
add r8b, al ; tmp = s[i] + s[j]
inc r10b ; i++
movzx rax, byte ptr [rsp + r8] ; rax = s[tmp]
xor byte ptr [rcx + UNROLL_I], al ; *data ^= rax => *data ^= s[s[i] + s[j]]
; Repeat the block for the next set of unrolled instructions
movzx r8, byte ptr [r10]
add r11b, r8b
movzx rax, byte ptr [r11]
mov byte ptr [r10], al
mov byte ptr [r11], r8b
add r8b, al
inc r10b
movzx rax, byte ptr [rsp + r8]
xor byte ptr [rcx + UNROLL_I + 1], al ;
; Repeat the block for the next set of unrolled instructions
movzx r8, byte ptr [r10]
add r11b, r8b
movzx rax, byte ptr [r11]
mov byte ptr [r10], al
mov byte ptr [r11], r8b
add r8b, al
inc r10b
movzx rax, byte ptr [rsp + r8]
xor byte ptr [rcx + UNROLL_I + 2], al ;
; Repeat the block for the next set of unrolled instructions
movzx r8, byte ptr [r10]
add r11b, r8b
movzx rax, byte ptr [r11]
mov byte ptr [r10], al
mov byte ptr [r11], r8b
add r8b, al
inc r10b
movzx rax, byte ptr [rsp + r8]
xor byte ptr [rcx + UNROLL_I + 3], al ;
; Update UNROLL_I for the next iteration
UNROLL_I = UNROLL_I + 4
我的代码的目的
使用通用寄存器而不是使用特定的寄存器如r9和r11,使用通用寄存器(rax、rbx、rcx等)给编译器在寄存器分配方面有更多的自由。
内存访问优化最大限度地减少内存访问。如果可能,将值加载到寄存器中并执行多个操作,然后再存储回内存。