%r10 是调用者保存的寄存器,所以需要在调用其他方法前后保存。
如果您避免在其中保留任何珍贵的东西,则不会!您陷入了由笨拙的“调用者保存”术语造成的陷阱,好像这意味着您必须保存它,而不是让 tmp 值消失。我更喜欢call-preserved vs. call-clobbered
选择一个像 RBX 这样的调用保留寄存器,用于保存您希望在调用中保留的值,并在函数的开始/结束时保存/恢复它一次。就像您对 RBP 所做的那样。您可以使用 R10 做到这一点,因为您的函数所做的唯一调用是对其自身。但是,如果您想完全遵循 x86-64 System V 调用约定(即仅假设调用自己会保留调用保留寄存器),请使用 RBX、RBP 或 R12..R15。 What registers are preserved through a linux x86-64 function call
作为一种优化,您可以只使用 1 个调用保留寄存器,首先将其用于 N,然后用于第一个 Fib 结果。
只是为了好玩,我将输入 N 设为 32 位整数;无需使用 64 位整数输入; Fib(94) 溢出一个 64 位整数!当我处理 N(32 位寄存器)与 Fib(N)(64 位寄存器)时,这应该会让我更容易看到。请记住,写入 32 位寄存器会隐式零扩展至完整的 64 位寄存器。
## Recursive is a terribly inefficient way to implement Fibonacci
## but if you must do it this way (without memoization or anything), this may be less bad than most, only using 16 bytes of stack per depth of call.
recursive_fib: # unsigned long fib(unsigned N)
cmp $1, %edi
ja .Lnon_basecase
mov %edi, %eax ## bugfix: Fib(0) = 0, Fib(1) = 1
ret
.Lnon_basecase:
push %rbx ###### save a call-preserved register
mov %edi, %ebx # keep a copy of N for later
dec %edi
call recursive_fib # Fib(N-1)
lea -2(%rbx), %edi # read N
mov %rax, %rbx # replace RBX with Fib(N-1)
call recursive_fib # Fib(N-2)
add %rbx, %rax # retval = Fib(N-2) + Fib(N-1)
pop %rbx ###### restore caller's RBX
ret
您可以当然将 RDI 溢出/重新加载到第一个 call 的堆栈中,就像编译器会将 mov 放入您之前使用 sub $16, %rsp 保留的空间中一样,或者使用实际推送/弹出。 如果大型函数中的调用保留寄存器用完,则需要这样做。
(但希望您可以选择溢出主要读取的内容:重新读取内存很便宜,更新内存会造成存储/重新加载延迟瓶颈。这就是编译器具有智能寄存器分配算法的原因)
(您只需要 8 个字节的空间,但编译器会在调用之前维护 x86-64 System V 的 16 字节 RSP 对齐。这意味着在另一个调用之前任何函数中的总 RSP 偏移量是 8 + 16*x,如果你以 push %rbp 开头,然后你需要留下 16 个字节。手动知道你只是在调用你自己,并且这个函数不关心堆栈对齐。)
顺便说一句,对于使用最后两个值的更简单的循环,递归斐波那契需要 O(Fib(N)) 时间与 O(N)。有趣的事实是,xadd %rdx, %rax 在循环中实现了斐波那契,或者使用 2 个 add 指令展开。 Assembly Language (x86): How to create a loop to calculate Fibonacci sequence