【问题标题】:Why does gnu_inline attribute affects code generation so much compared to general inlining?与一般内联相比,为什么 gnu_inline 属性对代码生成的影响如此之大?
【发布时间】:2019-04-27 20:12:59
【问题描述】:

为什么使用extern inline __attribute__((gnu_inline)) 而不是static inline 会对GCC 8.3 代码生成产生如此大的影响?

The example code 基于 glibc bsearch 代码(使用 -O3 构建):

#include <stddef.h>

extern inline __attribute__((gnu_inline))
void *bsearch (const void *__key, const void *__base, size_t __nmemb, size_t __size,
   int (*__compar)(const void *, const void *))
{
    size_t __l, __u, __idx;
    const void *__p;
    int __comparison;

    __l = 0;
    __u = __nmemb;
    while (__l < __u) {
        __idx = (__l + __u) / 2;
        __p = (void *) (((const char *) __base) + (__idx * __size));
        __comparison = (*__compar) (__key, __p);
        if (__comparison < 0)
            __u = __idx;
        else if (__comparison > 0)
            __l = __idx + 1;
        else
            return (void *) __p;
    }

  return NULL;
}

static int comp_int(const void *a, const void *b)
{
    int l = *(const int *) a, r = *(const int *) b;
    if (l > r) return 1;
    else if (l < r) return -1;
    else return 0;
}

int *bsearch_int(int key, const int *data, size_t num)
{
    return bsearch(&key, data, num, sizeof(int), &comp_int);
}

为bsearch_int函数生成的代码是:

bsearch_int:
        test    rdx, rdx
        je      .L6
        xor     r8d, r8d
.L5:
        lea     rcx, [rdx+r8]
        shr     rcx
        lea     rax, [rsi+rcx*4]
        cmp     DWORD PTR [rax], edi
        jl      .L3
        jg      .L10
        ret
.L10:
        mov     rdx, rcx
.L4:
        cmp     rdx, r8
        ja      .L5
.L6:
        xor     eax, eax
        ret
.L3:
        lea     r8, [rcx+1]
        jmp     .L4

如果我使用static inline 而不是extern inline __attribute__((gnu_inline)),我会得到更大的代码:

bsearch_int:
        xor     r8d, r8d
        test    rdx, rdx
        je      .L11
.L2:
        lea     rcx, [r8+rdx]
        shr     rcx
        lea     rax, [rsi+rcx*4]
        cmp     edi, DWORD PTR [rax]
        jg      .L7
        jl      .L17
.L1:
        ret
.L17:
        cmp     r8, rcx
        jnb     .L11
        lea     rdx, [r8+rcx]
        shr     rdx
        lea     rax, [rsi+rdx*4]
        cmp     edi, DWORD PTR [rax]
        jg      .L12
        jge     .L1
        cmp     r8, rdx
        jnb     .L11
.L6:
        lea     rcx, [r8+rdx]
        shr     rcx
        lea     rax, [rsi+rcx*4]
        cmp     DWORD PTR [rax], edi
        jl      .L7
        jle     .L1
        mov     rdx, rcx
        cmp     r8, rdx
        jb      .L6
.L11:
        xor     eax, eax
.L18:
        ret
.L12:
        mov     rax, rcx
        mov     rcx, rdx
        mov     rdx, rax
.L7:
        lea     r8, [rcx+1]
        cmp     r8, rdx
        jb      .L2
        xor     eax, eax
        jmp     .L18

是什么让 GCC 在第一种情况下生成如此短的代码?

注意事项:

  • Clang 似乎不受此影响。

【问题讨论】:

标签: c gcc inline compiler-optimization


【解决方案1】:

下面的答案是基于revision 2 of the question,而修订版 3 根据这个答案改变了问题的含义,之后下面的大部分答案似乎有点脱离上下文。根据第 2 版保留此答案。


来自6.31.1 Common Function Attributes of GCC's manual [强调我的]:

gnu_inline

此属性应与也声明的函数一起使用 使用 inline 关键字。它指示 GCC 将函数 视为 如果它是在 gnu90 模式下定义的,即使在 C99 或 gnu99 中编译 模式。

...

并且,来自Section 6.42 An Inline Function is As Fast As a Macro [强调我的]:

当一个函数同时是inline和static时,如果所有调用 函数被集成到调用者中,函数的地址为 从未使用过,则函数自己的汇编代码永远不会 参考。在这种情况下,GCC 并不实际输出汇编程序 函数的代码,除非您指定选项 -fkeep-inline-functions。

...

本节的其余部分特定于 GNU C90 内联。

当inline 函数不是static 时,编译器必须 假设可能有来自其他源文件的调用;由于全球 符号在任何程序中只能定义一次,函数不能 在其他源文件中定义,因此其中的调用不能 融合的。因此,一个非static inline 函数总是 以通常的方式自行编译。

如果您在函数中同时指定 inline 和 extern 定义,则定义仅用于内联。在没有 case 是自己编译的函数,即使你引用它 明确地址。这样的地址成为外部引用,如 如果你只声明了函数,而没有定义它。

...

这里的关键是gnu_inline 属性只会对以下两种情况产生影响,其中 GNU C90 内联将适用:

  • 同时使用extern 和inline,并且
  • 仅使用inline。

正如预期的那样,我们看到这两者之间生成的程序集存在很大差异。

但是,当使用 static 和 inline 时,GNU C90 内联规则不适用(或者更确切地说,没有专门涵盖这种情况),这意味着 gnu_inline 属性无关紧要。

确实,这两个签名导致相同的程序集:

static inline __attribute__((gnu_inline))
void *bsearch ...

static inline
void *bsearch ...

由于extern inline 和static inline 使用两种不同的内联方法(分别是GNU C90 内联策略和更现代的内联策略),因此可以预期生成的程序集在这两者之间可能会略有不同。尽管如此,与仅使用 inline 时相比,这两者产生的汇编输出要少得多(在这种情况下,如上所述,该函数始终是自行编译的)。

【讨论】:

  • 在这两种情况下都不会生成函数体,输出中的唯一函数是bsearch_int。您是否暗示 GNU C90 内联有一个单独的代码生成器,并且它以某种方式向优化器传达了更多信息?我希望输出是相同的,因为优化器无论如何都会做同样的工作,并且细微的差异通常并不重要(尽管在我的情况下,即使变量声明被移到外部范围,Clang 输出也会发生明显变化,所以我可以期待随机变化在输出中)。
  • @StaceyGirl 可能,是的。请注意,如果我们删除inline __attribute__((gnu_inline)) 并将bsearch 仅标记为static,则会为static 生成相同的程序集,这使我相信这里有两种完全不同的优化方案,其中一个是编译器利用我们的信息 (extern inline __attribute__((gnu_inline))) 结合 GNU C90 内联,它完全忽略了我们的 inline 提示(static inline 和 static 产生相同的结果,因此优化器试图找出问题本身而不是我们的提示)。
  • 我重新表述了这个问题,以表明我对这里的内联程序感兴趣。让我们看看是否有人可以提供更多关于为什么会发生这种情况的信息。
  • @StaceyGirl 并不是说​​您的编辑在某种程度上改变了整篇文章的含义(在给出答案之后),而且我在这里的大部分答案现在都没有实际意义。不过我也很感兴趣,看看有没有人可以提供更多细节。
  • 是的,很抱歉。我虽然第一次说的很清楚。
【解决方案2】:

它只编译是因为你没有使用任何优化并且内联没有激活。例如,尝试使用 -O1,您的代码将根本无法编译。

代码不同,因为当您使用静态时,编译器不必关心调用约定,因为该函数对其他编译单元不可见。

【讨论】:

  • 我使用-O3 编译,内联很有魅力。这也是为什么编译器无论如何都不需要关心调用约定的原因——输出中唯一的函数是bsearch_int,它没有特定于 GCC 的属性。并且代码编译找到所有优化级别。我将指向 godbolt.org 的链接移到更靠近问题开头的位置,以便更清楚。
猜你喜欢
  • 2011-09-10
  • 2010-10-02
  • 2019-06-24
  • 1970-01-01
  • 2019-12-26
  • 2015-10-13
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多