C语言 为什么与一般内联相比gnu_inline属性对代码生成的影响如此之大?



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

示例代码基于 glibcbsearch代码(使用-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似乎没有受到此影响。

它只会编译,因为您不使用任何优化并且内联未处于活动状态。例如,尝试使用 -O1,您的代码根本无法编译。

代码是不同的,因为当您使用 static 时,编译器不必关心调用约定,因为该函数对另一个编译单元不可见。

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


从 6.31.1 GCC 手册的通用函数属性 [强调我的]:

gnu_inline

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

并且,从第 6.42 节中,内联函数与宏一样快 [强调我的]:

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

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

inline函数未static时,编译器必须 假设可能存在来自其他源文件的调用;自全球以来 符号在任何程序中只能定义一次,函数不得 在其他源文件中定义,因此其中的调用不能 综合。因此,staticinline函数始终 以通常的方式自行编译

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

这里的关键是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时相比,这两种函数产生的程序集输出要少得多(在这种情况下,如上所述,函数总是自行编译)。

最新更新