在我尝试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次大约需要多长时间”的问题。
发布于 2022-01-01 02:41:51
大多数现代的微体系结构都有一个特殊的呼叫/返回预测器,在实际程序中往往是相互匹配的。(对于许多调用站点的函数来说,任何其他方法都很难预测返回:它是一个间接分支。)
通过手动处理返回地址,您将使那些返回预测错误。因此,每个ret都会导致一个分支的错误预测,除了没有使用x30的那个分支。
但是,如果您使用的是一个间接分支,而不是一个专门被认为是ret成语的分支,例如br x30,则CPU使用其标准的间接分支预测方法,当br重复到相同的位置时,这种方法做得很好。
google快速搜索从ARM中为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微体系结构如何在必须回滚的call或ret指令的误判执行中保持其预测准确性的一些细节。
(x86有一个实际的ret操作码;ARM64是相同的:ret操作码类似于br,但暗示这是一个函数返回。其他一些RISCs,如RISC-V,没有单独的操作码,只需假设具有链接寄存器的分支到寄存器就是一个返回。)
https://stackoverflow.com/questions/70546711
复制相似问题