BR/RET 从人为的子程序到修改后的 return 地址时 return 的时间差异

BR/RET timing discrepancy when returning from contrived subroutine to a modified return address

在我尝试使用 64 位 ARM 架构的过程中,我注意到一个特殊的速度差异,这取决于 brret 是否用于子例程中的 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'

这个子程序的目的是通过fooreturn指令使foo的调用者“重新进入”foow0次首先调用 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 微体系结构如何在面对错误推测执行 callret 必须回滚的指令。

(x86 有一个实际的 ret 操作码;ARM64 是相同的:ret 操作码类似于 br,但提示这是一个函数-return。其他一些 RISC,如 RISC-V 没有单独的操作码,只是假设使用 link 寄存器的分支到寄存器是 return。)