【问题标题】:Why does GCC insert seemingly non-essential instructions before a printf call?为什么 GCC 在 printf 调用之前插入看似非必要的指令?
【发布时间】:2011-09-04 11:43:25
【问题描述】:

我正在尝试自学 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

现在上面代码中的几乎所有内容对我来说都是有意义的,除了 main 下的前两个。虽然这是我在不剥离东西的情况下得到的。

.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,我理解它是一个函数的返回值。但是,如果该函数无论如何都会破坏它,为什么 GCC 会关心清除它呢?

【问题讨论】:

标签: gcc assembly x86


【解决方案1】:

第一条指令是将%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,我理解它是一个函数的返回值。但是,如果该函数无论如何都会破坏它,为什么 GCC 会关心清除它呢?

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

【讨论】:

  • 谢谢!您回答的第二部分是我正在寻找的信息类型。我并不是想让我的代码更快,我只是想弄清楚为什么它正在做它正在做的事情。尽管正如有人已经提到的那样,这可能不值得。
  • @spa 您问题的第一部分与编译器实现中所谓的寄存器分配有关。当您在没有优化的情况下编译代码时,寄存器分配可能非常愚蠢。但是,如果您要求编译器通过-O(例如-O3)优化您的代码,那么它将使用更智能的寄存器分配算法。
  • @spartacus,在调优性能(计算机体系结构中的任何微小变化,回到平方)时,在汇编器中重写代码是绝对禁忌,在汇编器中编写性能关键的 sn-ps是最后的手段。看看 Bentley 的“Programming Pearls”(cs.bell-labs.com/cm/cs/pearls)。
  • @vonbrand 我同意你关于不使用汇编程序编写性能的观点。然而,这不是我正在做的。这对我来说纯粹是一个学术练习,用于了解编译器为什么会这样做。
【解决方案2】:

几个经验法则:

  1. 如果您担心代码的效率,请不要费心查看未优化的输出。
  2. 始终衡量而不是假设您在汇编语言级别的“改进”会提高性能。

即使在优化的代码中,当不需要破坏寄存器时,您也可能会看到看似不必要的指令,例如“xor %eax,%eax”。这些指令通过通知管道在该点之外不存在该寄存器的数据依赖性来发挥特殊作用。在现代乱序处理器中,内核的管道在当前 EIP 之前推测性地执行 许多 指令。以这种方式显式减少数据依赖有助于推测机制,尤其是可以提高紧密循环中的性能。

在其他情况下,编译器显然会采取迂回的方法,而实际上它正在尝试将手头的工作与目标内核管道中可用的并行执行单元相匹配。并行调度的更多指令通常比串行化的更少指令完成得更快。

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

【讨论】:

    【解决方案3】:

    因为 GCC 是一个编译器,而编译器是愚蠢的。

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

    【讨论】:

    • 是的,我知道大多数编译器都是愚蠢的。我想我只是认为这是有原因的。
    • 除非您要求编译器花费额外的资源编写更智能的代码,否则它生成的代码既快又脏(而且通常是愚蠢的)。很长一段时间以来,编译器都能够生成比有经验的汇编程序员直接编写的代码更好的代码(除非人类花时间仔细编写尽可能好的代码,而且几十行需要几个小时,可能平均比编译器高几个百分点)。现代 CPU 是非常复杂的野兽,与入门编程课程中教授的幼稚想法完全不同。
    【解决方案4】:

    您查看的是优化后的输出,还是未优化的(这基本上是将 C 代码天真地翻译成汇编程序)?这有很大的不同,因为优化器通常非常擅长应用您描述的相同类型的规则。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-04-13
      • 2010-09-13
      • 2011-07-09
      • 1970-01-01
      • 2016-09-22
      • 1970-01-01
      相关资源
      最近更新 更多