在 ARM 汇编中接触更少的寄存器是否更有效率?

Is it more efficient to touch fewer registers in ARM assembly?

我刚刚开始通过 Raspbian 学习 Assembly 并有一个快速的问题:在 Assembly 中保存寄存器 space 的效率如何?例如,如果我想做一个快速加法,

中是否有有意义的差异?
mov r1, #5
mov r2, #3
add r1, r1, r2

mov r1, #5
mov r2, #3
add r3, r1, r2     @ destination in a new register that wasn't previously used

(不同寄存器存储除外)?

冒着被更了解理论方面的人打倒的风险,使用更多的寄存器可以更快 - 这就是架构设计面临压力以包含更多寄存器的原因之一(比较T32/A32/A64 随着架构实现成本的增加,可寻址核心寄存器的范围)。

在架构层面,核心寄存器都是等价的(只要操作码可以寻址它们)——即一些指令可能只允许访问低 8 个寄存器。

在微架构层面,给予某些寄存器优惠待遇是非常不寻常的。 ARMv7-M 及相关架构级别的优惠待遇示例之一是异常 push/pop 行为。编译器可以很容易地利用这种优化(通过避免插入一些垫片代码)。

更高性能的处理器实际上包含比体系结构寄存器更多的物理寄存器,并自动分配这些寄存器以提供具有更多逻辑寄存器的一些性能优势。

在您的示例中,您的第一个代码片段向 CPU 明确指示第一个 r1 值将来永远不会被使用。在第二个代码片段中,您在剩下的时间里 r1 == 5 有点受阻 - 没有办法向前看并预测您是否会再次使用它。

所以:

  • 更多的寄存器允许更快的数据(单周期)和潜在的乱序执行
  • 重新使用寄存器可能会在不重命名寄存器的情况下激活广泛发行机器中的互锁
  • 重新使用寄存器可以打破依赖链并在更高性能的处理器上释放更多物理寄存器。

对于 A53,我猜测根本没有区别,直到您的软件用完寄存器(除非您稍后想要 5 的值)。

使用与输入相同的输出寄存器在 ARM1 上没有固有的缺点。不过,我也不认为有任何内在优势。 在一般情况下,当我们谈论编写指令不必等待的寄存器时,事情会变得更有趣(即不是输入).

使用尽可能多的寄存器来保存指令。 (注意调用约定,但是:如果你使用的多于 r0..r3,你必须 save/restore 你使用的额外的,如果你想调用你的函数来自 C)。具体来说,通常针对最低的 dynamic 指令数进行优化;做一些额外的设置/清理以在循环内保存指令通常是值得的。

而且不仅仅是为了保存指令:software pipelining 隐藏加载延迟对于流水线顺序执行 CPU 具有潜在价值。例如如果你在一个数组上循环,从现在开始将你需要 2 次迭代的值加载到一个寄存器中,并且在那之前不要碰它。 (展开循环)。有序 CPU 只能按顺序 start 指令,但它们可能会乱序完成。例如缓存中未命中的加载不会使 CPU 停止,直到您在它未准备就绪时尝试读取它。我认为您可以假设像现代 ARM 一样的高性能有序 CPUs 将具有任何必要的记分牌来跟踪哪些寄存器正在等待 ALU 或加载结果准备就绪。

如果不真正进行完整的软件流水线操作,您有时可以通过加载块、填充块、存储块来获得类似的结果。例如为大副本优化的 memcpy 可能会在其展开的主循环中加载 12 个寄存器,然后存储这 12 个寄存器。因此同一寄存器的加载和存储之间的距离仍然足够大,至少可以隐藏 L1 缓存加载延迟。


当前(?)Raspberry Pi 板(RPi 3+) use ARM Cortex-A53 内核,2 宽超标量有序微体系结构。

任何执行乱序执行的 ARM 核心(如 Cortex-A57)都将使用 register renaming to make WAW (write-after-write) and WAR hazards a non-issue. (https://en.wikipedia.org/wiki/Hazard_(computer_architecture)#Data_hazards)。

在像 A53 这样的有序核心上,WAR 绝对不是问题:在较早的指令有机会从那里读取其操作数之前,后面的指令不可能写入寄存器。

但是 WAW 风险可能会限制 CPU 一次 运行 两条指令的能力 。这仅在编写您尚未阅读的寄存器时才有意义。 add r1, r1, r2 必须等待 r1 准备好才能开始执行,因为它是一个输入。

例如,如果您有这段代码,我们可能 实际上 看到在 2 条指令中写入相同的输出寄存器可能会 运行 对性能产生负面影响同一个循环。我不知道 Cortex-A53 或任何其他有序 ARM 如何处理此问题,但另一个双问题有序 CPU(1993 年的 Intel P5 Pentium)不会将写入同一寄存器的指令配对(Agner Fog's x86 uarch guide)。第二个必须等待一个周期才能开始(但可以与之后的指令配对)。

@ possible WAW hazard
adds  r3, r1, r2      @ set flags, we don't care about the r3 output
add   r3, r1, #5      @ now actually calculate an integer result we want

如果您使用不同的虚拟输出寄存器,它们可以在同一时钟周期内启动。(或者如果您使用 cmn r1, r2(比较-negated),你可以从 r1 - (-r2) 中设置标志而不用写输出,according to the manual 与从 r1 + r2 中设置标志相同。)但可能有一些情况你可以来不能用 cmpcmntst (ANDS) 或 teq (EORS) 指令代替。

我希望乱序的 ARM 可以在同一个周期内多次重命名同一个寄存器(OoO x86 CPUs 可以做到这一点)以完全避免 WAW危害。


我不知道保留一些寄存器对微架构有什么好处"cold"。

在 CPU 上进行寄存器重命名,通常这是通过物理寄存器文件完成的,甚至最近未修改的架构寄存器(如 r3)也需要 PRF 条目来保存最后写入它的任何指令的值,无论那是多久以前的。所以写一个寄存器总是分配一个新的物理寄存器,并且(最终)释放保存旧值的物理寄存器。不管旧值是否也刚刚被写入,或者它是否具有该值很长一段时间。

英特尔 P6 系列确实使用了 "retirement register file" 来保存退役状态,与乱序后端中的 "live" 值分开。但是它将这些实时寄存器值与生成它们的 uop 一起保存在 ROB 中(而不是对 PRF 条目的引用),因此它不能 运行 在后端被重命名之前离开物理寄存器满的。查看 http://blog.stuffedcow.net/2013/05/measuring-rob-capacity/ 更多有趣的 x86 CPU 实验测量 ROB 与 PRF 的乱序限制 window 其他使用 PRF 的 x86 CPUs 的大小。

事实上,由于退休寄存器文件上的读取端口有限,P6 系列(PPro 到 Nehalem)在读取太多尚未的寄存器时实际上可能会停止最近写在一个问题组中。 (请参阅 Agner Fog 的微架构指南,寄存器读取停顿。)但我认为这不是其他微架构上的典型问题,就像任何乱序的 ARM 内核一样。在循环外的寄存器中设置常量/循环不变量,在循环内自由使用。


脚注 1:这通常适用于所有架构,但也有例外。我知道的唯一一个是非常特殊的情况:在最近的 Intel x86 CPUs(64 位模式下)mov eax, eax(1 个周期延迟)比 mov ecx, eax(0 个周期)慢延迟)用于 t运行 将 64 位寄存器转换为 32 位,因为移动消除仅在不同寄存器之间起作用。 ()

使用 arm 时,效率主要来自调用约定,在正常管道内容之外(添加 xx、r1、r2 必须暂停才能完成 mov r2、xx)。

代码如此之少,两个块都是正确的解决方案,具体取决于问题。如果试图避免使用堆栈并使用流行的调用约定将信息保持在 4 个寄存器内,则重新使用一个寄存器而不是燃烧另一个寄存器可能是正确的,也可能是不正确的。

所有其他因素保持不变,不计算流水线设计中的任何因素,手臂没有什么神奇的东西会限制你,它不是像 CISC 这样的微编码设计,你可能有特定内核的特定性能规则.即使使用单个寄存器文件并且没有微编码,任何处理器都可以有流水线规则,但是寄存器在 arm 上应该是相等的。

而且 arm 很容易测试,看看你是否有性能问题,但你必须小心你的基准测试,不要最终测量其他东西并认为它是被测指令。