这样做确实很有意义。但我认为最简单的答案是正在使用所有其他寄存器。为了使用其他一些寄存器,您需要将其压入堆栈。
编译器足够聪明。跟踪编译器寄存器中的内容有些微不足道,这不是问题。一般来说,不一定是 x86 特定的,尤其是当您有更多寄存器(比 x86)时,您将有一些用于输入的寄存器(在您的调用约定中),一些您可以丢弃,这可能与输入与否,有些你不能丢弃,你必须先保存它们。有些指令集有特殊的寄存器,必须用这个来自动递增,那个用来间接寄存器,等等。
如果不是微不足道的话,你肯定会让编译器为 arm 生成代码,例如输入和可回收寄存器是同一组的情况,但这意味着如果你调用另一个函数并正确创建调用函数返回后需要保存一些东西使用:
unsigned int more_fun ( unsigned int );
unsigned int fun ( unsigned int x )
{
return(more_fun(x)+x);
}
00000000 <fun>:
0: e92d4010 push {r4, lr}
4: e1a04000 mov r4, r0
8: ebfffffe bl 0 <more_fun>
c: e0840000 add r0, r4, r0
10: e8bd4010 pop {r4, lr}
14: e12fff1e bx lr
我告诉过你这是微不足道的。现在要向后使用您的论点,为什么他们不只是将 r0 压入堆栈并稍后将其弹出,为什么要压入 r4?不是 r0-r3 用于输入并且是易失性的,r0 是适合时的返回寄存器,r4 几乎一直要保留(我认为是一个例外)。
因此假定 r4 被调用者或某些调用者使用,调用约定规定你不能丢弃它,你必须保留它,所以你必须假设它被使用。您可以丢弃 r0-r3,但您不能使用其中之一,因为被调用者也可以丢弃它们,因此在这种情况下,我们需要获取传入的值 x 并使用它(传递它)并在返回后保留它所以他们两者都做了,“使用另一个寄存器移动”但为了做到这一点,他们保留了另一个寄存器。
为什么在这种情况下将 r4 保存到堆栈中很明显,您可以将其与返回地址一起保存,特别是 arm 希望您始终以 64 位块的形式使用堆栈,因此理想情况下一次使用两个寄存器或至少保持它在 64 位边界上对齐,所以无论如何你都必须保存 lr,所以即使他们没有,他们也会推送其他东西,在这种情况下,保存 r4 是免费的,因为他们需要保存 r0 并同时使用它。 r4 或 r5 或以上是不错的选择。
顺便说一句,上面的 x86 编译器看起来很像。
0000000000000000 <fun>:
0: 53 push %rbx
1: 89 fb mov %edi,%ebx
3: e8 00 00 00 00 callq 8 <fun+0x8>
8: 01 d8 add %ebx,%eax
a: 5b pop %rbx
b: c3 retq
展示他们推动不需要保留的东西:
unsigned int more_fun ( unsigned int );
unsigned int fun ( unsigned int x )
{
return(more_fun(x)+1);
}
00000000 <fun>:
0: e92d4010 push {r4, lr}
4: ebfffffe bl 0 <more_fun>
8: e8bd4010 pop {r4, lr}
c: e2800001 add r0, r0, #1
10: e12fff1e bx lr
没有理由保存 r4,他们只是需要一些寄存器来使堆栈对齐,所以在这种情况下选择了 r4,这个编译器的某些版本你会看到使用了 r3 或其他一些寄存器。
记住人类(仍然)编写编译器和优化器等。所以他们为什么会这样以及为什么这对那个人或那些人来说真的是一个问题,我们无法真正告诉你他们在想什么。这肯定不是一项简单的任务,但不难采用合理大小的函数和/或项目并找到机会手动调整编译器输出以改进它。当然,美在旁观者的眼中,一种对改善的定义是另一种对恶化的定义。一种指令组合可能使用较少的总指令字节,因此按照程序大小标准“更好”,另一种可能使用或可能不使用更多指令或字节,但执行速度更快,一个可能具有较少的内存访问,但以理想执行的指令为代价更快,等等。
有数百个通用寄存器的架构,但我们每天接触产品的大多数都没有那么多,所以你通常可以在一个函数中创建一个函数或一些代码,其中有很多变量在运行,你必须开始保存到堆栈中间函数。所以你不能总是在函数的开头和结尾保存几个寄存器来给你更多的工作寄存器中间函数,如果你需要的中间函数的工作寄存器数量比你拥有的寄存器多。实际上需要一些练习才能编写不需要太多寄存器的优化代码,但是一旦您开始通过检查编译器的输出来了解编译器的工作原理,您就可以编写像上面那样的琐碎函数来防止优化或强制保存寄存器中间函数等
归根结底,要让编译器有点理智,它需要一个调用约定,它可以防止作者发疯,编译器也不会成为编码和管理的噩梦。调用约定非常清楚地定义了任何易失性寄存器的输入和输出寄存器,以及必须保留的寄存器。
unsigned int fun ( unsigned int x, unsigned int y, unsigned int z )
{
unsigned int a;
a=x<<y;
a+=(y<<z);
a+=x+y+z;
return(a);
}
00000000 <fun>:
0: e0813002 add r3, r1, r2
4: e0833000 add r3, r3, r0
8: e0832211 add r2, r3, r1, lsl r2
c: e0820110 add r0, r2, r0, lsl r1
10: e12fff1e bx lr
只花了几秒钟,但本可以更加努力。我没有推过总共四个寄存器,因为我有四个变量。而且我没有调用任何函数,因此编译器可以在依赖关系解决时根据需要随意丢弃 r0-r3。所以我不必为了创建临时存储而保存 r4,它不必使用堆栈它只是优化了执行顺序以例如释放 r2,z 变量,以便以后它可以使用 r2 作为中间变量, a 的实例之一等于某事。将其减少到四个寄存器,而不是烧掉第五个。
如果我的代码更有创意并且我添加了对函数的调用,我可以让它烧掉更多的寄存器,你会看到即使在最后一种情况下,编译器也没有任何问题可以跟踪什么是哪里,当您使用编译器时,您会看到没有理由他们必须在同一寄存器中保持您的高级语言变量完整无缺,更不用说按照您编写代码的相同顺序执行(只要它是合法),但它们仍然受调用约定的支配,如果只有一些寄存器被认为是易失性的,并且如果您在代码中的某个时间从您的函数调用一个函数,那么您必须保留该内容所以你不能将它们用作长期存储,并且那些非易失性的已经被认为已被消耗,因此必须保留它们才能使用它们,然后它部分成为性能问题,它是否成本更高(大小,速度等)以即时保存到堆栈中,或者我可以预先以一种可能减少指令或不可见和/或通过更大的传输而不是单独的、效率较低的中间功能传输而消耗更少时钟的方式预先提供服务?
我已经说过七次了,但最重要的是该编译器(版本)和目标(以及命令行选项/默认值)的调用约定。如果您有易失性寄存器(通用寄存器的任意调用约定事物,而不是硬件/ISA 事物)并且您没有调用任何其他函数,那么它们很容易使用并为您节省昂贵的堆栈(内存)事务。如果您正在呼叫某人,那么他们可能会被他们丢弃,因此他们可能不再是免费的,这取决于您的代码。非易失性寄存器被认为是调用者消耗的,所以你必须烧掉堆栈操作才能使用它们,它们不是免费使用的。然后它变成了关于何时何地使用堆栈、push、pop 和 mov 的性能。即使使用相同的约定,也不会期望两个编译器生成相同的代码,但是您可以在上面看到,制作测试函数,编译它们并检查输出,在这里和那里调整以浏览和绕过它有点微不足道(编译器、版本和目标以及约定和命令行选项)优化器。