从堆栈中弹出不需要的值,或者在 386+ CPU 上向 SP 添加立即数是否更快?
Is it faster to pop unneeded values from the stack, or add an immediate constant to SP on a 386+ CPU?
我的代码目标是 386+(通常是 DOSBox,偶尔是 Pentium MMX)CPU,但我只使用 8086 功能集来实现兼容性。我的代码是为非多任务环境(MS-DOS 或 DOSBox)编写的。
在嵌套循环中,我经常发现自己将 CX
重新用于更深层次的循环计数器。我 PUSH
它在嵌套循环的顶部, POP
它在执行 LOOP
之前。
有时 CX
以外的条件达到 0 会终止这些内部循环。然后我留下了不必要的循环计数器,有时还有更多的变量,坐在我需要清理的堆栈上。
是直接给 SP
添加一个常量更快,还是 POP
这些不需要的值?
我知道最快的方法是将 CX
存储在循环顶部的备用寄存器中,然后在 LOOP
执行之前将其恢复,完全放弃堆栈,但我经常没有备用寄存器。
这是一段代码,我在其中向 SP
添加了一个常量以避免一些 POP
指令:
FIND_ENTRY PROC
;SEARCHES A SINGLE SECTOR OF A DIRECTORY LOADED INTO secBuff FOR A
;SPECIFIED FILE/SUB DIRECTORY ENTRY
;IF FOUND, RETURNS THE FILE/SUB DIRECTORY'S CLUSTER NUMBER IN BX
;IF NOT FOUND, RETURNS 0 IN BX
;ALTERS BX
;EXPECTS A FILE NAME STRING INDEX NUMBER IN BP
;EXPECTS A SECTOR OF A DIRECTORY (ROOT, OR SUB) TO BE LOADED INTO secBuff
;EXPECTS DS TO BE LOADED WITH varData
push ax
push cx
push es
push si
push di
lea si, fileName ;si -> file name strings
mov ax, 11d ;ax -> file name length in bytes/characters
mul bp ;ax -> offset to file name string
add si, ax ;ds:si -> desired file name as source input
;for CMPS
mov di, ds
mov es, di
lea di, secBuff ;es:di -> first entry in ds:secBuff as
;destination input for CMPS
mov cx, 16d ;outer loop cntr -> num entries in a sector
ENTRY_SEARCH:
push cx ;store outer loop cntr
push si ;store start of the file name
push di ;store start of the entry
mov cx, 11d ;inner loop cntr -> length of file name
repe cmpsb ;Do the strings match?
jne NOT_ENTRY ;If not, test next entry.
pop di ;di -> start of the entry
mov bx, WORD PTR [di+26] ;bx -> entry's cluster number
add sp, 4 ;discard unneeded stack elements
pop di
pop si
pop es
pop cx
pop ax
ret
NOT_ENTRY:
pop di ;di -> start of the entry
add di, 32d ;di -> start of next entry
pop si ;si -> start of file name
pop cx ;restore the outer loop cntr
loop ENTRY_SEARCH ;loop till we've either found a match, or
;have tested every entry in the sector
;without finding a match.
xor bx, bx ;if we're here no match was found.
;return 0.
pop di
pop si
pop es
pop cx
pop ax
ret
FIND_ENTRY ENDP
如果您想编写高效的代码,与 相比,pop 与 add 是一个非常小的问题,并且优化其他所有内容(见下文)。
如果需要超过 1 个 pop
,请始终使用 add sp, imm
。或者 sub sp, -128
通过仍然使用 imm8 来节省代码大小。或者一些 CPU 可能更喜欢 lea
而不是 add/sub。 (例如,gcc 在 -mtune=atom
的情况下尽可能使用 LEA)。当然,这需要 16 位模式的地址大小前缀,因为 [sp+2]
不是有效的寻址模式。
除此之外,没有一个答案既适用于实际的 386 又适用于像 Haswell 或 Skylake 这样的现代 x86! 很多 之间的微架构变化 CPU。现代 CPUs 将 x86 指令解码为内部类似 RISC 的微指令。有一段时间,使用简单的 x86 指令很重要,但现在现代 CPUs 可以在一条指令中表示大量工作,因此更复杂的 x86 指令(如 push
或 add
带有内存源操作数)是单个 uop 指令。
现代 CPUs(从 Pentium-M 开始)有一个堆栈引擎,不需要单独的 uop 来实际更新无序核心中的 RSP/ESP/SP。当您使用非堆栈指令(push/pop / call/ret 以外的任何指令)read/write RSP 时,Intel 的实现需要堆栈同步 uop,这就是 pop
有用的原因,尤其是在推送或调用之后执行此操作时。
当需要单个 8 字节偏移量时,clang 使用 push
/pop
对齐 x86-64 代码中的堆栈。 .
但如果您关心性能,loop
is slow and should be avoided in the first place,更不用说循环计数器的 push/pop 了! 对 inner/outer 循环使用不同的 regs .
基本上,就优化而言,您在错误的道路上走得太远了,所以真正的答案只是指向您 http://agner.org/optimize/, and other performance links in the x86 tag wiki。由于对现代 CPUs 的所有部分寄存器错误依赖,16 位代码很难获得良好的性能,但对代码大小有一些影响,您可以在必要时使用 32 位操作数大小来打破这些。 (例如 xor ebx,ebx
)
当然,如果您针对 DOSBOX 进行了优化,那么它就不是真正的 CPU 并且是模拟的。所以loop
可能很快!如果有人分析或编写了 DOSBOX 的 CPU 模拟器的优化指南,请 IDK。但我建议学习在真正的现代硬件上什么是快速的;那更有趣。
我的代码目标是 386+(通常是 DOSBox,偶尔是 Pentium MMX)CPU,但我只使用 8086 功能集来实现兼容性。我的代码是为非多任务环境(MS-DOS 或 DOSBox)编写的。
在嵌套循环中,我经常发现自己将 CX
重新用于更深层次的循环计数器。我 PUSH
它在嵌套循环的顶部, POP
它在执行 LOOP
之前。
有时 CX
以外的条件达到 0 会终止这些内部循环。然后我留下了不必要的循环计数器,有时还有更多的变量,坐在我需要清理的堆栈上。
是直接给 SP
添加一个常量更快,还是 POP
这些不需要的值?
我知道最快的方法是将 CX
存储在循环顶部的备用寄存器中,然后在 LOOP
执行之前将其恢复,完全放弃堆栈,但我经常没有备用寄存器。
这是一段代码,我在其中向 SP
添加了一个常量以避免一些 POP
指令:
FIND_ENTRY PROC
;SEARCHES A SINGLE SECTOR OF A DIRECTORY LOADED INTO secBuff FOR A
;SPECIFIED FILE/SUB DIRECTORY ENTRY
;IF FOUND, RETURNS THE FILE/SUB DIRECTORY'S CLUSTER NUMBER IN BX
;IF NOT FOUND, RETURNS 0 IN BX
;ALTERS BX
;EXPECTS A FILE NAME STRING INDEX NUMBER IN BP
;EXPECTS A SECTOR OF A DIRECTORY (ROOT, OR SUB) TO BE LOADED INTO secBuff
;EXPECTS DS TO BE LOADED WITH varData
push ax
push cx
push es
push si
push di
lea si, fileName ;si -> file name strings
mov ax, 11d ;ax -> file name length in bytes/characters
mul bp ;ax -> offset to file name string
add si, ax ;ds:si -> desired file name as source input
;for CMPS
mov di, ds
mov es, di
lea di, secBuff ;es:di -> first entry in ds:secBuff as
;destination input for CMPS
mov cx, 16d ;outer loop cntr -> num entries in a sector
ENTRY_SEARCH:
push cx ;store outer loop cntr
push si ;store start of the file name
push di ;store start of the entry
mov cx, 11d ;inner loop cntr -> length of file name
repe cmpsb ;Do the strings match?
jne NOT_ENTRY ;If not, test next entry.
pop di ;di -> start of the entry
mov bx, WORD PTR [di+26] ;bx -> entry's cluster number
add sp, 4 ;discard unneeded stack elements
pop di
pop si
pop es
pop cx
pop ax
ret
NOT_ENTRY:
pop di ;di -> start of the entry
add di, 32d ;di -> start of next entry
pop si ;si -> start of file name
pop cx ;restore the outer loop cntr
loop ENTRY_SEARCH ;loop till we've either found a match, or
;have tested every entry in the sector
;without finding a match.
xor bx, bx ;if we're here no match was found.
;return 0.
pop di
pop si
pop es
pop cx
pop ax
ret
FIND_ENTRY ENDP
如果您想编写高效的代码,与
如果需要超过 1 个 pop
,请始终使用 add sp, imm
。或者 sub sp, -128
通过仍然使用 imm8 来节省代码大小。或者一些 CPU 可能更喜欢 lea
而不是 add/sub。 (例如,gcc 在 -mtune=atom
的情况下尽可能使用 LEA)。当然,这需要 16 位模式的地址大小前缀,因为 [sp+2]
不是有效的寻址模式。
除此之外,没有一个答案既适用于实际的 386 又适用于像 Haswell 或 Skylake 这样的现代 x86! 很多 之间的微架构变化 CPU。现代 CPUs 将 x86 指令解码为内部类似 RISC 的微指令。有一段时间,使用简单的 x86 指令很重要,但现在现代 CPUs 可以在一条指令中表示大量工作,因此更复杂的 x86 指令(如 push
或 add
带有内存源操作数)是单个 uop 指令。
现代 CPUs(从 Pentium-M 开始)有一个堆栈引擎,不需要单独的 uop 来实际更新无序核心中的 RSP/ESP/SP。当您使用非堆栈指令(push/pop / call/ret 以外的任何指令)read/write RSP 时,Intel 的实现需要堆栈同步 uop,这就是 pop
有用的原因,尤其是在推送或调用之后执行此操作时。
clang 使用 push
/pop
对齐 x86-64 代码中的堆栈。
但如果您关心性能,loop
is slow and should be avoided in the first place,更不用说循环计数器的 push/pop 了! 对 inner/outer 循环使用不同的 regs .
基本上,就优化而言,您在错误的道路上走得太远了,所以真正的答案只是指向您 http://agner.org/optimize/, and other performance links in the x86 tag wiki。由于对现代 CPUs 的所有部分寄存器错误依赖,16 位代码很难获得良好的性能,但对代码大小有一些影响,您可以在必要时使用 32 位操作数大小来打破这些。 (例如 xor ebx,ebx
)
当然,如果您针对 DOSBOX 进行了优化,那么它就不是真正的 CPU 并且是模拟的。所以loop
可能很快!如果有人分析或编写了 DOSBOX 的 CPU 模拟器的优化指南,请 IDK。但我建议学习在真正的现代硬件上什么是快速的;那更有趣。