“推”“POP”还是“移动”?

时间:2017-01-10 00:46:45

标签: gcc assembly optimization nasm inline-assembly

当涉及临时存储寄存器中的现有值时,所有现代编译器(至少是我经历过的编译器)都执行PUSH和POP指令。但是,如果数据可用,为什么不将数据存储在另一个寄存器中呢?

那么,现有值的临时存储应该在哪里?堆叠或注册

考虑以下第一个代码:

MOV ECX,16
LOOP:
PUSH ECX    ;Value saved to stack       
...     ;Assume that here's some code that must uses ECX register
POP ECX     ;Value released from stack  
SUB ECX,1
JNZ LOOP

现在考虑第二个代码:

MOV ECX,16
LOOP:
MOV ESI,ECX ;Value saved to ESI register    
...     ;Assume that here's some code that must uses ECX register
MOV ECX,ESI ;Value returned to ECX register
SUB ECX,1
JNZ LOOP

毕竟,上面哪一个代码更好,为什么?

我个人认为第一个代码的大小更好,因为PUSH和POP只占用1个字节而MOV需要2个字节;第二个代码在速度上更好,因为在寄存器之间移动的数据比内存访问更快。

5 个答案:

答案 0 :(得分:1)

使用寄存器要快一点,但要求您跟踪哪些寄存器可用,并且您可以用完寄存器。此外,此方法不能递归使用。此外,如果使用INT或CALL调用子例程,某些寄存器将被删除。

堆栈(POP和PUSH)的使用可以根据需要多次使用(只要你没有用完堆栈空间),此外它还支持递归逻辑。您可以使用INT或CALL安全地使用堆栈,因为按照惯例,任何子例程都应保留其自己的堆栈部分,并且必须将其恢复到先前的状态(否则RET指令将失败)。

答案 1 :(得分:1)

在考虑速度时,你总是要记住一种比例感。

如果正在编译的函数调用其他函数, 那些pushpop指令可能无关紧要, 与在它们之间执行的指令数量相比较。

编译器编写者知道,在这种情况下,这是非常常见的,不应该是penny-wise and pound-foolish

答案 2 :(得分:1)

通过使用PUSH和POP,您可以保存至少一个寄存器。如果您使用有限的可用寄存器,这将是重要的。另一方面,是的,有时使用MOV的速度更快,但您还必须记住哪个寄存器用作临时存储。如果要存储稍后需要处理的多个值,这将很难

答案 3 :(得分:1)

这样做确实很有道理。但我认为最简单的答案是所有其他寄存器都在使用。为了使用其他寄存器,您需要将其推入堆栈。

编译器非常聪明。跟踪编译器寄存器中的内容有点微不足道,这不是问题。说来一般不一定是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并且都使用它(传递它)并在返回后保留它所以他们两个都做了,&#34;用另一个寄存器做了一个移动&#34;但为了做到这一点,他们保留了其他登记册。

为什么在这种情况下将r4保存到堆栈非常明显,你可以使用返回地址预先保存它,特别是arm希望你总是在64位块中使用堆栈,这样两个寄存器一次最好或者在至少保持它在64位边界上对齐,所以你必须保存lr,所以即使他们没有,他们也会推送别的东西,在这种情况下,保存r4是免费赠品,因为他们需要保存r0并同时使用它。 r4或r5或以上是一个不错的选择。

BTW看起来像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或其他一些寄存器。

记住人类(仍然)编写编译器和优化器等。所以他们为什么这个以及为什么这对于那个人或那些人来说真的是一个问题,我们真的不能告诉你他们在想什么。这当然不是一项简单的任务,但是不难采取合理大小的函数和/或项目,并找到机会手动调整编译器输出,以改进它。当然,美丽是旁观者的眼睛,改善的另一个定义是另一个更糟糕的定义。一个指令混合可能使用较少的总指令字节,因此更好&#34;根据程序大小标准,另一个可能会或可能不会使用更多指令或字节,但执行速度更快,可能会有更少的内存访问,但理由上执行得更快,等等。

有几百个通用寄存器的架构,但我们每天触摸产品的大多数都没有那么多,所以你通常可以在一个函数中创建一个函数或一些代码,它们在飞行中具有如此多的变量必须开始保存到堆栈中功能。所以你不能总是只在函数的开头和结尾保存一些寄存器,以便在函数中间提供更多工作寄存器,如果你需要mid函数的工作寄存器数量多于你的寄存器。实际上需要一些练习才能编写没有优化的代码,以至于不需要太多寄存器,但是一旦你开始通过检查输出来看看编译器是如何工作的,你可以编写像上面那样的简单函数来防止优化或强制保存函数等中的寄存器

在一天结束时,编译器要有点理智,它需要一个调用约定,它使作者不会发疯,编译器成为一个噩梦,代码和管理。调用约定非常明确地定义输入和输出寄存器的任何易失性寄存器,以及必须保留的寄存器。

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作为中间变量,等于某事的一个例子。将其保留到四个寄存器而不是烧第五个寄存器。

如果我对我的代码更有创意,并且我添加了对函数的调用,我可以让它烧掉更多的寄存器,你会看到即使在最后一种情况下,编译器也没有任何问题跟踪什么就是在哪里,你会看到当你使用编译器时,没有理由他们必须保持你的高级语言变量在相同的寄存器中保持完整,而执行次数与你编写代码的顺序相同(只要它是合法的,但它们仍然受调用约定的支配,如果只有一些寄存器被认为是易失性的,并且如果你在代码中的某个时间从函数调用函数,那么你必须保留该内容因此你不能将它们用作长期存储,并且那些非易失性存储已经被认为是消耗的,所以它们必须被保留以便使用它们,然后它变得部分成为性能问题,它是否成本更高(尺寸,速度) ,等)在飞行中保存到堆栈或我可以预先以可能减少指令或可能不可见的方式提供前置和/或使用较大的传输消耗较少的时钟而不是单独的,效率较低的中间功能传输?

我现在已经说了七次,但底线是该编译器(版本)和目标(以及命令行选项/默认值)的调用约定。如果你有易失性寄存器(通用寄存器的任意调用约定,而不是硬件/ ISA的东西)并且你没有调用任何其他函数,那么它们易于使用并节省昂贵的堆栈(内存)事务。如果你打电话给某人,那么他们可能会被他们摧毁,因此他们可能不再是免费的,这取决于你的代码。非易失性寄存器被调用者认为是消耗的,因此您必须刻录堆栈操作才能使用它们,它们不能自由使用。然后它就变成了在何时何地使用堆栈,推送和弹出以及移动的性能。即使他们使用相同的约定,也不会有两个编译器生成相同的代码,但是你可以看到上面制作测试函数,编译它们并检查输出,在这里和那里进行调整以及在那里进行导航是有点微不足道的(编译器,版本和目标以及约定和命令行选项)优化器。

答案 4 :(得分:0)

基于数十年代码生成专家的工作,相信优化编译器的工作。

它们填充尽可能多的寄存器并在需要时扩展到堆栈,比较不同的选项。并且他们还关心存储值以便以后重用与重新计算值之间的权衡。

没有单一的规则“寄存器与堆栈”,这是全局优化的问题,考虑到处理器的特性。一般而言,没有单一的“最佳解决方案”,因为它取决于您的“最佳”标准。

除非能够找到非常有创意的解决方法(或者只利用您所知的数据属性),否则您无法击败编译器。