9

考虑这段代码:

// foo.cxx
int last;

int next() {
  return ++last;
}

int index(int scale) {
  return next() << scale;
}

使用 gcc 7.2 编译时:

$ g++ -std=c++11 -O3 -fPIC

这发出:

next():
    movq    last@GOTPCREL(%rip), %rdx
    movl    (%rdx), %eax
    addl    $1, %eax
    movl    %eax, (%rdx)
    ret
index(int):
    pushq   %rbx
    movl    %edi, %ebx
    call    next()@PLT    ## next() not inlined, call through PLT
    movl    %ebx, %ecx
    sall    %cl, %eax
    popq    %rbx
    ret

但是,当使用 clang 3.9 编译具有相同标志的相同代码时:

next():                               # @next()
    movq    last@GOTPCREL(%rip), %rcx
    movl    (%rcx), %eax
    incl    %eax
    movl    %eax, (%rcx)
    retq

index(int):                              # @index(int)
    movq    last@GOTPCREL(%rip), %rcx
    movl    (%rcx), %eax
    incl    %eax              ## next() was inlined!
    movl    %eax, (%rcx)
    movl    %edi, %ecx
    shll    %cl, %eax
    retq

gccnext()通过 PLT 调用,clang 内联它。last两者仍然从 GOT 中查找。对于在 linux 上编译,clang 进行优化是否正确,而 gcc 错过了简单的内联,或者 clang 进行优化是错误的,或者这纯粹是 QoI 问题?

4

1 回答 1

15

我认为该标准没有详细说明。它只是说,如果符号在不同的翻译单元中具有外部链接,那么它是同一个符号。这使得 clang 的版本是正确的。

从那时起,据我所知,我们已经超出了标准。编译器的选择在他们认为有用的-fPIC输出上有所不同。

请注意,g++ -c -std=c++11 -O3 -fPIE输出:

0000000000000000 <_Z4nextv>:
   0:   8b 05 00 00 00 00       mov    0x0(%rip),%eax        # 6 <_Z4nextv+0x6>
   6:   83 c0 01                add    $0x1,%eax
   9:   89 05 00 00 00 00       mov    %eax,0x0(%rip)        # f <_Z4nextv+0xf>
   f:   c3                      retq   

0000000000000010 <_Z5indexi>:
  10:   8b 05 00 00 00 00       mov    0x0(%rip),%eax        # 16 <_Z5indexi+0x6>
  16:   89 f9                   mov    %edi,%ecx
  18:   83 c0 01                add    $0x1,%eax
  1b:   89 05 00 00 00 00       mov    %eax,0x0(%rip)        # 21 <_Z5indexi+0x11>
  21:   d3 e0                   shl    %cl,%eax
  23:   c3                      retq

所以 GCC确实知道如何优化它。它只是在使用时选择不使用-fPIC. 但为什么?我只能看到一种解释:可以在动态链接期间覆盖符号,并始终如一地看到效果。该技术称为符号插入

在共享库中,如果调用index全局可见,gcc 必须考虑可能被插入的可能性。所以它使用PLT。但是,在使用时,不允许插入符号,因此 gcc 启用优化。nextnextnext-fPIE

那么clang错了吗?不,但 gcc 似乎为符号插入提供了更好的支持,这对于检测代码很方便。-fPIC如果一个人使用而不是-fPIE构建他的可执行文件,它会以一些开销为代价。


补充说明:

这篇来自一位 gcc 开发人员的博客文章中,他在文章结尾处提到:

在将一些基准与 clang 进行比较时,我注意到 clang 实际上忽略了 ELF 插入规则。虽然这是错误,但我决定向-fno-semantic-interpositionGCC 添加标志以获得类似的行为。如果不需要插入,ELF 的官方回答是使用隐藏可见性,如果需要导出符号,请定义别名。这并不总是手工完成的实际操作。

紧随其后,我进入了x86-64 ABI 规范。在第 3.5.5 节中,它确实要求所有调用全局可见符号的函数都必须通过 PLT(它根据内存模型定义要使用的确切指令序列)。

因此,尽管它不违反 C++ 标准,但忽略语义插入似乎违反了 ABI。


最后一句话:不知道把这个放在哪里,但你可能会感兴趣。我会为您省去转储,但我对 objdump 和编译器选项的测试表明:

在 gcc 方面:

  • gcc -fPIC访问last通过GOT,调用next()通过PLT。
  • gcc -fPIC -fno-semantic-interposition:last通过 GOT,next()被内联。
  • gcc -fPIE: last是IP相关的,next()是内联的。
  • -fPIE暗示-fno-semantic-interposition

在事情的铿锵声方面:

  • clang -fPIC: last通过 GOT,next()被内联。
  • clang -fPIE: last通过 GOT,next()被内联。

以及编译为 IP 相关的修改版本,在两个编译器上内联:

// foo.cxx
int last_ __attribute__((visibility("hidden")));
extern int last __attribute__((alias("last_")));

int __attribute__((visibility("hidden"))) next_()
{
  return ++last_;
}
// This one is ugly, because alias needs the mangled name. Could extern "C" next_ instead.
extern int next() __attribute__((alias("_Z5next_v")));

int index(int scale) {
  return next_() << scale;
}

基本上,这明确表明,尽管使它们在全球范围内可用,但我们使用这些符号的隐藏版本,将忽略任何类型的插入。然后,无论传递的选项如何,两个编译器都会完全优化访问。

于 2017-08-31T02:26:35.130 回答