assembly - 从人为的子例程返回到修改后的返回地址时,BR/RET 时序差异

标签 assembly arm arm64 branch-prediction

在我尝试 64 位 ARM 架构的过程中,我注意到一个特殊的速度差异,这取决于是使用 br 还是 ret 从子例程返回.

; 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 的调用者“重新输入” foo w0 次,方法是使 foo 返回到首先调用 foo 的指令(即紧接在 x30 指向的指令之前的指令)。通过一些粗略的计时,w0 是一些足够高的值,平均花费大约 1362 毫秒。奇怪的是,将第一个 ret 替换为 br x30 会使它的运行速度提高两倍,平均只需要 550 毫秒左右。

如果将测试简化为仅使用简单的 ret/br x30 重复调用子例程,则计时差异就会消失。是什么让上面设计的带有 ret 的子例程变慢了?

我在某种 ARMv8.2(Cortex-A76 + Cortex-A55)处理器上对此进行了测试。我不确定 big.LITTLE 会在多大程度上扰乱时间,但它们在多次运行中似乎非常一致。这绝不是一个真正的[微]基准测试,而是一个“如果运行 N 次大约需要多长时间”的东西。

最佳答案

大多数现代微体系结构都有一个特殊的调用/返回预测器,它们往往在实际程序中相互匹配。 (并且对于具有许多调用点的函数来说,很难以任何其他方式预测返回值:它是一个间接分支。)

通过手动处理返回地址,您会做出错误的返回预测。所以每次 ret 都会导致分支预测错误,除了你没有使用 x30 的那个。

但是,如果您使用间接分支而不是专门识别为 ret 习惯用法的分支,例如br x30,CPU 使用其标准的间接分支预测方法,当 br 重复到达同一位置时,该方法表现良好。


Google 快速搜索发现了一些来自 ARM for Cortex-R4 的信息,关于 32 位模式(4 入口循环缓冲区)微架构上的返回预测器堆栈: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,但提示这是一个函数返回。其他一些 RISC,如 RISC-V 没有单独的操作码,只是假设使用链接寄存器的分支到寄存器是一个返回。)

关于assembly - 从人为的子例程返回到修改后的返回地址时,BR/RET 时序差异,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/70546711/

相关文章:

assembly - 如果处理器不产生中断,系统软件必须做什么?

assembly - 了解aarch64汇编函数调用,栈是如何操作的?

ruby-on-rails - 在arm64上安装gem idn-ruby失败

NASM 中的编译错误 - Windows

c++ - 如何使用 CPUID 作为序列化指令?

linux - GNU 汇编程序是否添加了自己的入口点?

linux - arm64 和 armhf 有什么区别?

c - 在 gcc 内联汇编程序 (ARM) 中使用数组

arm - Buildroot包管理

debugging - 如何在ARM处理器上设置软件断点?