在英特尔 x64 汇编中执行函数的最快(CPU 方式)?
Fastest (CPU-wise) way to do functions in intel x64 assembly?
我一直在阅读有关汇编函数的内容,但我对是使用进入和退出还是仅使用 call/return 指令来快速执行感到困惑。一种方式快而另一种方式更小吗?例如,在不内联函数的情况下,在汇编中执行此操作的最快(stdcall)方法是什么:
static Int32 Add(Int32 a, Int32 b) {
return a + b;
}
int main() {
Int32 i = Add(1, 3);
}
使用 call
/ ret
,而不使用 enter
/ leave
或 push&pop rbp / mov rbp, rsp
创建堆栈帧。 gcc(默认 -fomit-frame-pointer
)只在函数中创建堆栈帧,这些函数在堆栈上进行可变大小分配。 This may make debugging slightly more difficult,因为 gcc 在使用 -fomit-frame-pointer
编译时通常会发出堆栈展开信息,但您的手写 asm 不会有那个。通常只有在 asm 中编写叶函数才有意义,或者至少是那些不调用许多其他函数的函数。
堆栈帧意味着您不必跟踪自函数进入以来堆栈指针发生了多少变化以访问堆栈上的内容(例如函数参数和局部变量的溢出槽)。 Windows 和 Linux/Unix 64 位 ABI 都将前几个参数传递到寄存器中,并且通常有足够多的寄存器,您不必将任何变量溢出到堆栈中。在大多数情况下,栈帧是指令的浪费。在 32 位代码中,ebp
可用(从 6 到 7 个 GP regs,不计算堆栈指针)比从 14 到 15 有更大的不同。当然,你仍然必须 push/pop
rbp但是,如果您确实使用它,因为在两个 ABI 中它都是一个被调用者保存的寄存器,函数不允许破坏。
如果您正在优化 x86-64 asm,您应该阅读 Agner Fog's guides, and check out some of the other links in the x86 标签 wiki。
您的函数的最佳实现可能是:
align 16
global Add
Add:
lea eax, [rdi + rsi]
ret
; the high 32 of either reg doesn't affect the low32 of the result
; so we don't need to zero-extend or use a 32bit address-size prefix
; like lea eax, [edi, esi]
; even if we're called with non-zeroed upper32 in rdi/rsi.
align 16
global main
main:
mov edi, 1 ; 1st arg in SysV ABI
mov esi, 3 ; 2nd arg in SysV ABI
call Add
; return value in eax in all ABIs
ret
align 16
OPmain: ; This is what you get if you don't return anything from main to use the result of Add
xor eax, eax
ret
这实际上 what gcc emits for Add()
, but it still turns main into an empty function, or into a return 4
if you return i
. clang 3.7 尊重 -fno-inline-functions
即使结果是编译时常量。它通过尾调用优化击败了我的 asm,jmp
ing 到 Add
.
请注意,Windows 64 位 ABI 为函数参数使用不同的寄存器。请参阅 x86 标记 wiki 或 Agner Fog 的 ABI 指南中的链接。 Assembler macros 可能有助于在 asm 中编写为其参数使用正确寄存器的函数,具体取决于您的目标平台。
我一直在阅读有关汇编函数的内容,但我对是使用进入和退出还是仅使用 call/return 指令来快速执行感到困惑。一种方式快而另一种方式更小吗?例如,在不内联函数的情况下,在汇编中执行此操作的最快(stdcall)方法是什么:
static Int32 Add(Int32 a, Int32 b) {
return a + b;
}
int main() {
Int32 i = Add(1, 3);
}
使用 call
/ ret
,而不使用 enter
/ leave
或 push&pop rbp / mov rbp, rsp
创建堆栈帧。 gcc(默认 -fomit-frame-pointer
)只在函数中创建堆栈帧,这些函数在堆栈上进行可变大小分配。 This may make debugging slightly more difficult,因为 gcc 在使用 -fomit-frame-pointer
编译时通常会发出堆栈展开信息,但您的手写 asm 不会有那个。通常只有在 asm 中编写叶函数才有意义,或者至少是那些不调用许多其他函数的函数。
堆栈帧意味着您不必跟踪自函数进入以来堆栈指针发生了多少变化以访问堆栈上的内容(例如函数参数和局部变量的溢出槽)。 Windows 和 Linux/Unix 64 位 ABI 都将前几个参数传递到寄存器中,并且通常有足够多的寄存器,您不必将任何变量溢出到堆栈中。在大多数情况下,栈帧是指令的浪费。在 32 位代码中,ebp
可用(从 6 到 7 个 GP regs,不计算堆栈指针)比从 14 到 15 有更大的不同。当然,你仍然必须 push/pop
rbp但是,如果您确实使用它,因为在两个 ABI 中它都是一个被调用者保存的寄存器,函数不允许破坏。
如果您正在优化 x86-64 asm,您应该阅读 Agner Fog's guides, and check out some of the other links in the x86 标签 wiki。
您的函数的最佳实现可能是:
align 16
global Add
Add:
lea eax, [rdi + rsi]
ret
; the high 32 of either reg doesn't affect the low32 of the result
; so we don't need to zero-extend or use a 32bit address-size prefix
; like lea eax, [edi, esi]
; even if we're called with non-zeroed upper32 in rdi/rsi.
align 16
global main
main:
mov edi, 1 ; 1st arg in SysV ABI
mov esi, 3 ; 2nd arg in SysV ABI
call Add
; return value in eax in all ABIs
ret
align 16
OPmain: ; This is what you get if you don't return anything from main to use the result of Add
xor eax, eax
ret
这实际上 what gcc emits for Add()
, but it still turns main into an empty function, or into a return 4
if you return i
. clang 3.7 尊重 -fno-inline-functions
即使结果是编译时常量。它通过尾调用优化击败了我的 asm,jmp
ing 到 Add
.
请注意,Windows 64 位 ABI 为函数参数使用不同的寄存器。请参阅 x86 标记 wiki 或 Agner Fog 的 ABI 指南中的链接。 Assembler macros 可能有助于在 asm 中编写为其参数使用正确寄存器的函数,具体取决于您的目标平台。