更高效的汇编代码?

时间:2017-04-20 03:14:46

标签: assembly i386 gcc4.7

我最近开始学习集会。只是想知道为什么这个组件是按照它的方式编写而不是替代#34;我的组件"我在下面列出。它削减了一条指令。有任何想法吗?在这种情况下,它是否太罕见?对于我来说,首先将3的值移至eax似乎是浪费。

C代码:

#include<stdio.h>

int main()
{
   int a = 1;
   int b = 3;
   a = a+b;
   return a;
}

大会:

Dump of assembler code for function main:
0x080483dc <+0>:    push   ebp
0x080483dd <+1>:    mov    ebp,esp
0x080483df <+3>:    sub    esp,0x10
0x080483e2 <+6>:    mov    DWORD PTR [ebp-0x4],0x1
0x080483e9 <+13>:   mov    DWORD PTR [ebp-0x8],0x3
0x080483f0 <+20>:   mov    eax,DWORD PTR [ebp-0x8]
0x080483f3 <+23>:   add    DWORD PTR [ebp-0x4],eax
0x080483f6 <+26>:   mov    eax,DWORD PTR [ebp-0x4]
0x080483f9 <+29>:   leave  
0x080483fa <+30>:   ret   

&#34;我的大会&#34;:

Dump of assembler code for function main:
0x080483dc <+0>:    push   ebp
0x080483dd <+1>:    mov    ebp,esp
0x080483df <+3>:    sub    esp,0x10
0x080483e2 <+6>:    mov    DWORD PTR [ebp-0x4],0x1
0x080483e9 <+13>:   mov    DWORD PTR [ebp-0x8],0x3
0x080483f0 <+20>:   add    DWORD PTR [ebp-0x4],DWORD PTR [ebp-0x8]
0x080483f3 <+23>:   mov    eax,DWORD PTR [ebp-0x4]
0x080483f6 <+26>:   leave
0x080483f9 <+29>:   ret   

1 个答案:

答案 0 :(得分:3)

正如Michael Petch在评论中已经说过的那样,真正的答案是你正在寻找未经优化的代码。编译器在未经优化的代码中做各种各样的......好的,效率低下的东西。有时它们会为编译速度而做。优化比盲目地将C代码转换为汇编指令花费更长的时间,因此当您需要原始速度时,关闭优化器并仅使用编译器:一个相对简单的指令转换器。编译器在未经优化的代码中执行低效操作的另一个原因是使调试更容易。例如,您的IDE可能允许您在C / C ++代码的每一行上设置断点。如果优化器已将多个C / C ++行转换为单个汇编指令,则设置要设置的断点(如果不是不可能的话)将更加困难。这就是为什么调试优化代码要困难得多,并且通常需要降低到原始程序集并进行地址级调试。

这里有两个死的赠品告诉你这是未经优化的代码:

  1. 使用leave指令,它基本上是x86的CISC日历史遗留物。过去的理念是拥有一堆执行复杂操作的指令,因此在函数开头使用enter指令来设置堆栈帧,并且leave指令提出了后方,撕下堆叠框架。这使得程序员在块结构语言中的工作变得更容易,因为您只需要编写一条指令来完成多个任务。问题是,因为至少386,可能是286,enter指令比使用更简单的单独指令做同样的事情要慢得多。 leave在386及更高版本上的速度也较慢,仅在您针对速度进行优化时才有用(因为它较小且不像enter那么慢)。

    < / LI>
  2. 正在设置堆栈帧的事实!在任何优化级别,32位x86编译器都不会费心生成设置堆栈帧的序言代码。也就是说,它不会保存EBP寄存器的原始值,也不会将EBP寄存器设置为函数入口处的堆栈指针(ESP)的位置。相反,它将执行“帧指针省略”优化(EBP寄存器称为“帧指针”),而不是使用EBP - 相对偏移来访问堆栈,它将只使用ESP - 相对抵消。这在16位x86代码中不可能实现,但它在32位代码中工作正常,它只需要更多的簿记,因为堆栈指针可能会发生变化,但帧指针可以保持不变。对于计算机/编译器而言,这样的簿记几乎不是人类的问题,因此这是一个明显的优化。

  3. “你的”程序集的另一个问题是你使用了无效的指令。 x86架构中没有指令 * 接受两个内存操作数。 At most, one of the operands can be a memory location. The other operand must either be a register or an immediate.

    此代码的第一个“优化”版本将类似于:

    ; Allocate 8 bytes of space on the stack for our local variables, 'a' and 'b'.
    sub  esp, 8
    
    ; Load the values of 'a' and 'b', storing them into the allocated locations.
    ; (Note the use of ESP-relative offsets, rather than EBP-relative offsets.)
    mov  DWORD PTR [esp],     1
    mov  DWORD PTR [esp + 4], 3
    
    ; Load the value of 'a' into a register (EAX), and add 'b' to it.
    ; (Necessary because we can't do an ADD with two memory operands.)
    mov  eax, DWORD PTR [esp]
    add  eax, DWORD PTR [esp + 4]
    
    ; The result is now in EAX, which is exactly where we want it to be.
    ; (All x86 calling conventions return integer-sized values in EAX.)
    
    ; Clean up the stack, and return.
    add  esp, 8
    ret
    

    我们已经“优化”了堆栈初始化序列,并且丢失了大量的漏洞。事情看起来很不错。实际上,如果您要声明ab变量volatile,这实际上就是编译器将生成的代码。但是,它们在原始代码中实际上volatile,这意味着我们可以将它们完全保存在寄存器中。这使我们无需执行任何昂贵的内存存储/加载,这意味着我们根本不需要分配或恢复堆栈空间!

    ; Load the 'a' and 'b' values into the EAX and EDX registers, respectively.
    mov  eax, 1
    mov  edx, 3
    
    ; Add 'b' to 'a' in a single operation, since ADD works fine with
    ; two register operands.
    add  eax, edx
    
    ; Return, with result in EAX.
    ret
    

    干净吧?这不仅简化了代码,而且实际上是一个巨大的性能胜利,因为我们将所有内容保存在寄存器中,而不必触摸慢速内存。那么,我们还能做些什么呢?请记住,ADD指令允许我们使用寄存器作为目标操作数,使用立即数作为源操作数。这意味着我们可以跳过MOV并执行:

    mov  eax, 1
    add  eax, 3
    ret
    

    这类似于你所期望的,比如说你在内存中已经存在一个常量3:

    add  DWORD PTR [esp + 4], 3
    

    但在这种情况下,优化编译器永远不会这样做。它实际上会超越你,意识到你正在添加编译时常量,并继续在编译时进行添加。因此,编译器的实际输出 - 实际上是编写此代码的最有效方式 - 将是:

    mov  eax, 4
    ret
    

    如何反高潮。 :-)最快的代码始终是不必执行的代码。

    * 至少,不是我现在能想到的。 x86 ISA是巨大的,所以几乎不可避免的是,它有一些黑暗的角落,我无法想到这个陈述是错误的。但是,你可以依赖它作为公理,这是真的。