如何恢复 x86-64 寄存器保存约定
How to restore x86-64 register saving conventions
fibonacci:
cmpq , %rdi
ja .recursive
movl , %eax
ret
.recursive:
push %rbp
push %r10
movq %rdi, %r10
leaq -2(%rdi), %rdi
call fibonacci
movq %rax, %rbp
leaq -1(%r10), %rdi
call fibonacci
addq %rbp, %rax
pop %r10
pop %rbp
ret
如何恢复 x86-64 寄存器保存约定。
我不确定该怎么做,但这是我的思路。 %r10是caller-saved寄存器,所以在调用其他方法前后都需要保存。在这种情况下,只有在递归调用斐波那契之后使用 %r10 时,我们才需要保存它。不过,我不确定在哪里添加说明来恢复寄存器保存约定。
%r10 is a caller-saved register, so it needs to be saved before and after calls to other methods.
如果你避免在里面放任何珍贵的东西,就不会!您正在陷入由笨拙的“调用者保存”术语所造成的陷阱,就好像这意味着您 必须 来保存它而不是让 tmp 值消亡。我更喜欢call-preserved vs. call-clobbered
选择一个像 RBX 这样的调用保留寄存器来保存你希望在调用中保留的值,save/restore它一次在start/end 你的函数。就像您对 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 , %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
放入 space 您之前使用 sub , %rsp
或实际 push/pop 保留。 如果您 运行 超出大型函数中的调用保留寄存器,则需要这样做。
(但希望你可以选择溢出一些读取最多的东西:重新读取内存很便宜,更新内存会造成 store/reload 延迟瓶颈。这就是编译器具有智能寄存器分配算法的原因)
(您只需要 space 的 8 个字节,但编译器会在调用之前保持 x86-64 系统 V 的 16 字节 RSP 对齐。这意味着在另一个调用之前任何函数中的总 RSP 偏移量是8 + 16*x
,如果你以 push %rbp
开头,那么你需要保留 16 个字节。通过手写你知道你只是在调用你自己,并且这个函数不关心堆栈对齐。)
顺便说一句,递归 Fibonacci 需要 O(Fib(N)) 时间,而 O(N) 对于使用最后 2 个值的更简单的循环。有趣的是,循环中的 xadd %rdx, %rax
实现斐波那契数列,或者用 2 add
指令展开。
fibonacci:
cmpq , %rdi
ja .recursive
movl , %eax
ret
.recursive:
push %rbp
push %r10
movq %rdi, %r10
leaq -2(%rdi), %rdi
call fibonacci
movq %rax, %rbp
leaq -1(%r10), %rdi
call fibonacci
addq %rbp, %rax
pop %r10
pop %rbp
ret
如何恢复 x86-64 寄存器保存约定。
我不确定该怎么做,但这是我的思路。 %r10是caller-saved寄存器,所以在调用其他方法前后都需要保存。在这种情况下,只有在递归调用斐波那契之后使用 %r10 时,我们才需要保存它。不过,我不确定在哪里添加说明来恢复寄存器保存约定。
%r10 is a caller-saved register, so it needs to be saved before and after calls to other methods.
如果你避免在里面放任何珍贵的东西,就不会!您正在陷入由笨拙的“调用者保存”术语所造成的陷阱,就好像这意味着您 必须 来保存它而不是让 tmp 值消亡。我更喜欢call-preserved vs. call-clobbered
选择一个像 RBX 这样的调用保留寄存器来保存你希望在调用中保留的值,save/restore它一次在start/end 你的函数。就像您对 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 , %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
放入 space 您之前使用 sub , %rsp
或实际 push/pop 保留。 如果您 运行 超出大型函数中的调用保留寄存器,则需要这样做。
(但希望你可以选择溢出一些读取最多的东西:重新读取内存很便宜,更新内存会造成 store/reload 延迟瓶颈。这就是编译器具有智能寄存器分配算法的原因)
(您只需要 space 的 8 个字节,但编译器会在调用之前保持 x86-64 系统 V 的 16 字节 RSP 对齐。这意味着在另一个调用之前任何函数中的总 RSP 偏移量是8 + 16*x
,如果你以 push %rbp
开头,那么你需要保留 16 个字节。通过手写你知道你只是在调用你自己,并且这个函数不关心堆栈对齐。)
顺便说一句,递归 Fibonacci 需要 O(Fib(N)) 时间,而 O(N) 对于使用最后 2 个值的更简单的循环。有趣的是,循环中的 xadd %rdx, %rax
实现斐波那契数列,或者用 2 add
指令展开。