与常规内联相比,为什么gnu_inline属性对代码生成的影响如此之大?

时间:2019-04-27 20:12:59

标签: c gcc inline compiler-optimization

为什么在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在第一种情况下生成了如此短的代码?

注意:

  • C语似乎不受此影响。

2 个答案:

答案 0 :(得分: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 [强调我的]:

  

当函数同时为inlinestatic时,如果所有调用   该功能已集成到调用方中,并且该功能的地址为   永远不要使用,那么函数自己的汇编代码就永远不会   参考。在这种情况下, GCC实际上不会输出汇编程序   该功能的代码,除非您指定该选项   -fkeep-inline-functions。

     

...

     

该部分的其余部分特定于GNU C90内联

     

如果inline函数不是static,则编译器必须   假设可能有来自其他源文件的调用;自全球   符号在任何程序中只能定义一次,该函数不能   在其他源文件中定义,因此其中的调用不能   集成。因此,static inline函数始终是   

     

如果在函数中同时指定了{strong> inlineextern   定义,则定义仅用于内联。在没有   情况是函数是单独编译的,即使您引用它也不是   明确地址。这样的地址成为外部引用,例如   如果您只声明了该函数,但尚未定义它。

     

...

这里的关键是gnu_inline属性仅对以下两种情况有效,其中GNU C90内联将适用:

  • 同时使用externinline,并且
  • 仅使用inline

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

但是,在使用staticinline时,GNU C90内联规则不适用(或者不专门涵盖这种情况),这意味着gnu_inline属性将不适用问题。

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

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

static inline
void *bsearch ...

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

答案 1 :(得分:0)

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

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