为什么在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在第一种情况下产生如此多的短代码?
笔记:
下面的答案基于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
时,如果对函数的所有调用都集成到调用者中,并且从不使用函数的地址,则永远不会引用函数自己的汇编代码。在这种情况下,除非指定选项-fkeep-inline-functions,否则GCC实际上不会为函数输出汇编代码。...
本节的其余部分特定于GNU C90内联。
当
inline
函数不是static
时,编译器必须假定可能存在来自其他源文件的调用;由于全局符号只能在任何程序中定义一次,因此不能在其他源文件中定义该函数,因此无法集成其中的调用。因此,非static
inline
函数总是以通常的方式自行编译。如果在函数定义中同时指定
inline
和extern
,则该定义仅用于内联。在任何情况下,函数都不会自行编译,即使您明确地引用其地址也是如此。这样的地址成为外部引用,就像您只声明了该函数一样,并且没有定义它。...
它们的关键在于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
相比,这两者产生的组件输出明显更少(在这种情况下,如上所述,该功能总是单独编译)。
它只会编译,因为您不使用任何优化,并且内联不活动。以-O1为例,您的代码根本不会编译。
代码是不同的,因为当您使用静态时,编译器不必关心调用约定,因为该函数对另一个编译单元是不可见的。