BR/RET 从人为的子程序到修改后的 return 地址时 return 的时间差异
BR/RET timing discrepancy when returning from contrived subroutine to a modified return address
在我尝试使用 64 位 ARM 架构的过程中,我注意到一个特殊的速度差异,这取决于 br
或 ret
是否用于子例程中的 return .
; Contrived for learning/experimenting purposes only, without any practical use
foo:
cmp w0, #0
b.eq .L0
sub w0, w0, #1
sub x30, x30, #4
ret
.L0:
ret ; Intentionally duplicated 'ret'
这个子程序的目的是通过foo
return指令使foo
的调用者“重新进入”foo
w0
次首先调用 foo
的指令(即 x30
指向的指令之前的指令)。通过一些粗略的计时,w0
是一些足够高的值,平均花费大约 1362 毫秒。奇怪的是,将第一个 ret
替换为 br x30
会使它 运行 快两倍,平均只需要 550 毫秒左右。
如果将测试简化为仅用 ret
/br x30
重复调用子程序,则时间差异就会消失。是什么让上面人为设计的子例程变慢了 ret
?
我在某种 ARMv8.2(Cortex-A76 + Cortex-A55)处理器上对此进行了测试。我不确定 big.LITTLE 会在多大程度上扰乱时间,但它们在多个 运行 上似乎非常一致。这绝不是一个真正的 [micro] 基准,而是一个“如果 运行 N 次,这大约需要多长时间”的事情。
大多数现代微体系结构都有一个特殊的调用预测器 / return,它们往往在实际程序中相互匹配。 (对于具有许多调用点的函数来说,以任何其他方式预测 returns 是很困难的:它是一个间接分支。)
通过手动使用 return 地址,您的 return 预测是错误的。所以每个 ret
都会导致分支预测错误,除了你没有玩 x30
.
的那个
但是,如果您使用间接分支而不是专门识别为 ret
成语的分支,例如br x30
,CPU 使用其标准的间接分支预测方法,当 br
重复到达同一位置时效果很好。
快速 google 搜索从 ARM 中为 Cortex-R4 找到了一些关于 32 位模式(4 入口循环缓冲区)微体系结构上的 return-预测器堆栈的信息:https://developer.arm.com/documentation/ddi0363/e/prefetch-unit/return-stack
对于 x86,https://blog.stuffedcow.net/2018/04/ras-microbenchmarks/ 是一篇关于一般概念的好文章,以及一些关于各种 x86 微体系结构如何在面对错误推测执行 call
或 ret
必须回滚的指令。
(x86 有一个实际的 ret
操作码;ARM64 是相同的:ret
操作码类似于 br
,但提示这是一个函数-return。其他一些 RISC,如 RISC-V 没有单独的操作码,只是假设使用 link 寄存器的分支到寄存器是 return。)
在我尝试使用 64 位 ARM 架构的过程中,我注意到一个特殊的速度差异,这取决于 br
或 ret
是否用于子例程中的 return .
; Contrived for learning/experimenting purposes only, without any practical use
foo:
cmp w0, #0
b.eq .L0
sub w0, w0, #1
sub x30, x30, #4
ret
.L0:
ret ; Intentionally duplicated 'ret'
这个子程序的目的是通过foo
return指令使foo
的调用者“重新进入”foo
w0
次首先调用 foo
的指令(即 x30
指向的指令之前的指令)。通过一些粗略的计时,w0
是一些足够高的值,平均花费大约 1362 毫秒。奇怪的是,将第一个 ret
替换为 br x30
会使它 运行 快两倍,平均只需要 550 毫秒左右。
如果将测试简化为仅用 ret
/br x30
重复调用子程序,则时间差异就会消失。是什么让上面人为设计的子例程变慢了 ret
?
我在某种 ARMv8.2(Cortex-A76 + Cortex-A55)处理器上对此进行了测试。我不确定 big.LITTLE 会在多大程度上扰乱时间,但它们在多个 运行 上似乎非常一致。这绝不是一个真正的 [micro] 基准,而是一个“如果 运行 N 次,这大约需要多长时间”的事情。
大多数现代微体系结构都有一个特殊的调用预测器 / return,它们往往在实际程序中相互匹配。 (对于具有许多调用点的函数来说,以任何其他方式预测 returns 是很困难的:它是一个间接分支。)
通过手动使用 return 地址,您的 return 预测是错误的。所以每个 ret
都会导致分支预测错误,除了你没有玩 x30
.
但是,如果您使用间接分支而不是专门识别为 ret
成语的分支,例如br x30
,CPU 使用其标准的间接分支预测方法,当 br
重复到达同一位置时效果很好。
快速 google 搜索从 ARM 中为 Cortex-R4 找到了一些关于 32 位模式(4 入口循环缓冲区)微体系结构上的 return-预测器堆栈的信息:https://developer.arm.com/documentation/ddi0363/e/prefetch-unit/return-stack
对于 x86,https://blog.stuffedcow.net/2018/04/ras-microbenchmarks/ 是一篇关于一般概念的好文章,以及一些关于各种 x86 微体系结构如何在面对错误推测执行 call
或 ret
必须回滚的指令。
(x86 有一个实际的 ret
操作码;ARM64 是相同的:ret
操作码类似于 br
,但提示这是一个函数-return。其他一些 RISC,如 RISC-V 没有单独的操作码,只是假设使用 link 寄存器的分支到寄存器是 return。)