为什么GCC在printf调用之前插入看似非必要的指令?

时间:2011-05-29 23:08:07

标签: gcc assembly x86

我正在尝试自己学习x86,我决定剖析一个简单的c程序,看看GCC输出了什么。该计划是这样的:

#include <stdio.h>
int main() {
  printf("%s","Hello World");
  return 0;
}

我用-S编译了代码,然后剥离了我认为不必要的东西并将汇编代码减少到了这个。

.pfArg:
.string "%s"
.text

.Hello:
.string "Hello World"
.text

.globl main
.type   main, @function

main:
pushq   %rbp        # push what was in base pointer onto stack
movq    %rsp, %rbp  # move stack pointer to base pointer
subq    $16, %rsp   # subtract 16 from sp and store in stack pointer

# prepare arguments for printf
movl    $.Hello, %esi   # put & of "Hello World" into %esi
movq    $.pfArg, %rdi   # put & of "%d" into %eax
call    printf
leave
ret

现在几乎上面代码中的所有内容都对我有意义,除了前两个主要内容。虽然这是我得到的,但没有剥离出来。

.LC0:
    .string "%s"

.LC1:
    .string "Hello World"
    .text

.globl main
    .type   main, @function

main:

.LFB0:
    pushq   %rbp        # push what was in base pointer onto stack
    movq    %rsp, %rbp  # move stack pointer to base pointer

  # prepare arguments for printf
    movl    $.LC0, %eax # put arg into %eax
    movl    $.LC1, %esi # put second arg into %esi
    movq    %rax, %rdi  # move value in %rax to %rdi ???? ( why not just put $.LCO into %rax directly )
    movl    $0, %eax    # clear out %eax ???? ( why do we need to clear it out )
    call    printf      
    movl    $0, %eax    # return 0
    leave
    ret

.LFE0:
    .size   main, .-main
    .ident  "GCC: (Ubuntu/Linaro 4.5.2-8ubuntu4) 4.5.2"
    .section    .note.GNU-stack,"",@progbits

我标有2条指令????我不明白

第一条指令是将%rax中的内容移动到%rdi中以准备printf调用。这很好,除了我们只是将$ .LC0(字符串“%s”)移动到%eax中。这似乎是不必要的,为什么我们不首先将$ .LC0移动到%rdi而不是将它移动到%eax然后转移到%rdi?

第二条指令是清除%eax,我理解它是函数的返回值。但是,如果该功能只是破坏它,那么为什么海湾合作委员会要小心清除它呢?

4 个答案:

答案 0 :(得分:6)

一条经验法则:

  1. 如果您担心有效的代码,请不要费心查看未经优化的输出。
  2. 始终衡量,永远不要假设您在汇编语言级别的“改进”提高了性能。
  3. 即使在优化的代码中,当没有功能需要破坏寄存器时,您可能会看到看似不必要的指令,例如“xor%eax,%eax”。这些指令通过告知管道在该点之外不存在该寄存器的数据依赖性而发挥特殊作用。在现代无序处理器中,核心的流水线推测性地在当前EIP之前执行许多指令。以这种方式明确地减少数据依赖性有助于推测机制,并且可以提高紧密循环中的性能。

    在其他情况下,编译器可能显然采用循环方法,实际上它正在尝试将手头的工作与目标核心管道中可用的并行执行单元进行匹配。并行调度的更多指令通常比序列化的指令更少完成。

    如果您真的想要挤出最后一滴性能,请在代码块之前和之后使用rdtsc指令来测量消耗的时钟数。要小心一点,因为rdtsc并没有严格按照周围的说明进行排序,但实际上测量它对1000个时钟范围内的任何东西都是准确的。

答案 1 :(得分:4)

您是在查看优化输出还是未优化(这基本上是C代码到汇编程序的简单翻译)?这会产生巨大的差异,因为优化器通常非常适合应用您描述的相同类型的规则。

答案 2 :(得分:3)

  

第一条指令是将%rax中的内容移至%rdi以准备printf来电。这一切都很好,除了我们刚刚将$.LC0(字符串"%s")移动到%eax。这似乎是不必要的,为什么我们不首先将$.LC0移到%rdi而不是将其移至%eax然后转移到%rdi

这可能是因为你没有进行优化编译。当我在Mac OS X v10.6.8上使用GCC 4.2.1编译您的示例时,我得到以下输出:

.globl _main
_main:
LFB3:
    pushq   %rbp
LCFI0:
    movq    %rsp, %rbp
LCFI1:
    leaq    LC0(%rip), %rsi
    leaq    LC1(%rip), %rdi
    movl    $0, %eax
    call    _printf
    movl    $0, %eax
    leave
    ret

如您所见,这些参数直接存储在%rsi%rdi中。

  

第二条指令正在清除%eax,我理解它是函数的返回值。但是,如果该功能只是破坏它,那么为什么海湾合作委员会要小心清除它呢?

因为x86_64 ABI指定如果函数采用变量参数,那么AL(它是%eax的一部分)应该保留用于该函数调用的参数的向量寄存器的数量。由于在调用printf()时没有指定浮点参数,因此不使用向量寄存器,因此AL%eax)被清零。我在回答另一个问题here时提供了更多示例。

答案 3 :(得分:2)

因为GCC是编译器,编译器很笨。

您可以使用-O2使GCC更智能。它开始使用优化技巧并减少冗余指令。