我正在研究 x86-64 NASM,这是目前的情况:
首先,我编写了一些直接、易于阅读的代码。然而,我发现了一些“聪明”的方法来初始化寄存器以减少指令长度。
我想知道这些聪明的东西是否能带来真正的回报,还是弊大于利。
这是第一个直接的代码:
.loop:
mov rax, -1
mov rdx, 1 ; **
mov rsi, 2 ; **
; ... loop body
dec rcx
jnz .loop
(**:汇编器实际上将这些行发出为
mov edx, 1
和 mov esi, 2
。后来我发现汇编器为我优化了它们,因为编写 edx/esi 会将 rdx/rsi 的高 32 位归零。)
这些是 17 个字节的开头和 5 个字节的结尾。
这是第二个巧妙的代码:
.loop:
xor eax, eax
dec rax
lea edx, [rax+2] ; ***
lea esi, [rdx+1] ; ***
; ... loop body
loop .loop
(***:我尝试了各种32位/64位寄存器的组合,这些是最短的指令长度)
这些是 11 个字节的开头和 2 个字节的结尾。
这样做是否是个好主意取决于您的目标。 通常,这不是一个好主意。
如果您的目标是易于理解,您应该避免这些技巧,因为它们会使您的代码更难理解。
如果您的目标是减少代码大小,那么使用这些技巧可能确实是个好主意。 不过,你可以做得比你已经做的更好;例如,您可以执行
or rax, -1
将 rax
设置为 -1
,仅用 4 个字节。
但是,通常目标是性能。 现在,当您优化性能时,一些技巧会有所帮助,但另一些技巧则有害。 特别是,您在问题中向我们展示的所有技巧都不利于性能:
清除然后递减寄存器所需的时间与直接将寄存器设置为
-1
一样长或稍慢,具体取决于微体系结构。 无论如何我都会避免它,因为两条指令比一条指令占用更多的解码器带宽。
从其他寄存器派生寄存器而不是直接设置它们本身并不会花费更多时间,但是当您引入对另一个寄存器的依赖性时,这些初始化现在必须在设置另一个寄存器之后执行,而不是并行执行。 这会降低无序架构的性能,应该避免。 设计您的代码,以便可以并行完成尽可能多的操作。
loop
指令速度很慢,应该避免使用。 但 dec
后跟一个条件分支也应该如此:当 dec
执行部分标志更新时,如果随后读取标志,某些微架构上会存在惩罚。 如果您想评估标记结果,请使用 sub rcx, 1
。
请注意,在优化性能时,有时优化大小可能仍然是一个好主意。 这是因为较长的代码序列会占用指令缓存中的更多空间,从而阻止缓存其他代码。 在热代码路径不完全适合 L1 指令缓存的大型程序中,性能可以从代码大小优化中受益,特别是在很少执行的冷路径中。 然而,评估这是一件棘手的事情,策略必须适应当前的情况。 在任何情况下,让基准指导您的决策。