gcc之inline关键字(汇编角度查看内联)

以下使用GNU89的标准

1.内联的定义:

内联就是一个关键字inline加载函数定义处,告诉编译器在编译的时候请对这个函数调用的地方进行内联调用(这里说的请,编译器可以拒绝这个操作因为内联函数的失败)

2.内联函数的作用

内联是为了节约函数的调用开销而诞生的,我们在调用一个普通函数的时候,存在的额外的开销(压栈、出栈)等。内联是让编译器使用内联的方式编译函数,在调用这个内联函数的时候,直接把这个函数展开,不存在压栈、出栈的操作,汇编中就是不断的寄存器的读取,内联就减少了这些寄存器的使用,一句话就是快速执行函数体

3.内联的使用

使用起来很简单直接在函数定义处加上inline关键字,注意是定义处

1. 例子1(普通函数)
#include <iostream>
inline void example() {
    std::cout << "example" << std::endl;
}
int main() {
    example();
    return 0;
}
  1. 例子2(类函数)

在类的头文件直接声明定义的不需要加参数会自动inline
但是如果是头文件声明(不加inline的类函数声明),.cpp定义那就还是需要函数定义加上inline

class Test {
public:
    Test() {  //类中直接实现自动内联
        
    }
    ~Test();
};
inline Test::~Test() { //类外定义实现,需要inline声明
    
}

4. gcc编译器编译内联代码

这里只说和inline有关的

  1. 想要实现程序内inline关键字的真正运行需要加上编译优化选项,默认的gcc编译的优化选项是-O0的,如果直接gcc test.c -o xx,这样是不内联的,有些版本还编译不过

  2. 想要编译通过需要使用 -O -O1 -O2 -O3 -Og,-O和-O1是相同的,2和3是优化的等级提升

  3. -O1中除了添加了对inline内联的支持还加入了-finline-functions-called-once, 官方解释

    -finline-functions-called-once
    
    Consider all functions called once for inlining into their caller even if they are not marked . If a call to a given function is integrated, then the function is not output as assembler code in its own right. staticinline
    
    Enabled at levels , , and , but not . -O1-O2-O3-Os-Og
    

    意思就是查看代码中所有的函数如果仅仅被调用了一次那么默认 inline内联

    如果有函数调用了这样的static修饰的函数,那么static 函数本身不会被汇编代码输出(默认的程序会保存到.text段),static和内联的事情我们内联的时候分析

  4. -Og

    在保持快速编译和良好调试体验的同时,提供合理的优化级别,这个会在-O1基础上去除finline-functions-called-once,因为finline-functions-called-once会让我们在函数调用一次的情况下,即使不加inline也会被优化被带inline

  5. 也说一下-O2,里面有一个-finline-functions

    -finline-functions
    
    Consider all functions for inlining, even if they are not declared inline. The compiler heuristically decides which functions are worth integrating in this way.
    
    If all calls to a given function are integrated, and the function is declared , then the function is normally not output as assembler code in its own right. `static`
    
    Enabled at levels , , . Also enabled by and . -O2-O3-Os-fprofile-use-fauto-profile
    

    意思就是考虑将所有的函数内联编译,即使他们没有加上inline。

    如果给定函数的调用集中一起,就是函数前面加了static修饰,这样这个函数最终就只有一个,就不会生成汇编代码,static和内联的事情我们内联的时候分析

    还有一个-finline-small-functions

    -finline-small-functions
    Integrate functions into their callers when their body is smaller than expected function call code (so overall size of program gets smaller). The compiler heuristically decides which functions are simple enough to be worth integrating in this way. This inlining applies to all functions, even those not declared inline.
    
    Enabled at levels , , . -O2-O3-Os
    

    编译器会看,当调用那些函数体长度小于调用者的函数调用的长度时,会直接内联插入,即使没有函数没有使用inline编译,这个之后进行说明

5. 汇编角度看加inline和不加的区别

下面我们从汇编层面看看,因为最终c程序是要变成汇编的

函数的实现就是getvalue对参数进行了加一并返回,主函数调用并打印2

1. 加入inline的函数

//test_inline.c
#include <stdio.h>
#include <stdlib.h>
inline int getValue(int i) {
        return i + 1;
}

int main() {
        int c = getValue(1);
        printf("start = %d\n", c);
        return 0;
}
  1. 不加inline的函数
//test.c
#include <stdio.h>
#include <stdlib.h>
int getValue(int i) {
        return i + 1;
}

int main() {
        int c = getValue(1);
        printf("start = %d\n", c);
        return 0;
}   
  1. 进行汇编编译

    1. 汇编编译的前提说明:

      1. 对不加inline的进行编译, 下面为了看见调试信息统一加上-g选项
      2. 使用先编译为可执行文件然后使用objdump -S xxx, 反汇编进行解读
      3. gcc编译器这里我们使用-Og选项
      4. 使用GNU89标准 , 选择gcc 编译标准, gcc xx -std=gnu89
      5. (!详情)[https://gcc.gnu.org/onlinedocs/gcc/C-Dialect-Options.html]
    2. 对没有加入inline的函数进行分析

      gcc -g -Og -std=gnu89 test.c -o test

      objdump -S test

      我们调重点看getValue和Main

000000000000066a <getValue>:
//test.c
#include <stdio.h>
#include <stdlib.h>
int getValue(int i) {
        return i + 1;
 66a:	8d 47 01             	lea    0x1(%rdi),%eax
}
 66d:	c3                   	retq   

000000000000066e <main>:

int main() {
 66e:	48 83 ec 08          	sub    $0x8,%rsp
        int c = getValue(1);
 672:	bf 01 00 00 00       	mov    $0x1,%edi
 677:	e8 ee ff ff ff       	callq  66a <getValue>
}

__fortify_function int
printf (const char *__restrict __fmt, ...)
{
  return __printf_chk (__USE_FORTIFY_LEVEL - 1, __fmt, __va_arg_pack ());
 67c:	89 c2                	mov    %eax,%edx
 67e:	48 8d 35 9f 00 00 00 	lea    0x9f(%rip),%rsi        # 724 <_IO_stdin_used+0x4>
 685:	bf 01 00 00 00       	mov    $0x1,%edi
 68a:	b8 00 00 00 00       	mov    $0x0,%eax
 68f:	e8 ac fe ff ff       	callq  540 <__printf_chk@plt>
        printf("start = %d\n", c);
        return 0;
}
 694:	b8 00 00 00 00       	mov    $0x0,%eax
 699:	48 83 c4 08          	add    $0x8,%rsp
 69d:	c3                   	retq   
 69e:	66 90                	xchg   %ax,%ax
可以看出

	1. 对应的函数代码存在汇编中

	2. 进行了callq  66a <getValue>的函数调用,还使用了 $0x1,%edi进行函数入栈

3. 我们再来看看加了inline的,这次test.c内容在函数前面加上inline

	gcc -Og -g test_inline.c -std=gnu89 -o test_inline

	objdump -S test_inline

	我们调重点看getValue和Main
000000000000066a <main>:
#include <stdlib.h>
inline int getValue(int i) {
	return i + 1;
}

int main() {
 66a:	48 83 ec 08          	sub    $0x8,%rsp
}

__fortify_function int
printf (const char *__restrict __fmt, ...)
{
  return __printf_chk (__USE_FORTIFY_LEVEL - 1, __fmt, __va_arg_pack ());
 66e:	ba 02 00 00 00       	mov    $0x2,%edx
 673:	48 8d 35 aa 00 00 00 	lea    0xaa(%rip),%rsi        # 724 <_IO_stdin_used+0x4>
 67a:	bf 01 00 00 00       	mov    $0x1,%edi
 67f:	b8 00 00 00 00       	mov    $0x0,%eax
 684:	e8 b7 fe ff ff       	callq  540 <__printf_chk@plt>
	int c = getValue(1);
	printf("start = %d\n", c);
	return 0;
}
 689:	b8 00 00 00 00       	mov    $0x0,%eax
 68e:	48 83 c4 08          	add    $0x8,%rsp
 692:	c3                   	retq   
 693:	66 2e 0f 1f 84 00 00 	nopw   %cs:0x0(%rax,%rax,1)
 69a:	00 00 00 
 69d:	0f 1f 00             	nopl   (%rax)

	1. 对应的函数代码存在汇编中

	2. 进行了mov    $0x2,%edx的直接计算

	2. 进直接进行了mov    $0x2,%edx的计算替换,没有的刚刚的call调用

6.inline和其他关键字的组合

常见的搭配有 static inline 、extern inline

看一下几个编译器的区别

GNU89:
inline: the function may be inlined (it's just a hint though). An out-of-line version is always emitted and externally visible. Hence you can only have such an inline defined in one compilation unit, and every other one needs to see it as an out-of-line function (or you'll get duplicate symbols at link time).
extern inline will not generate an out-of-line version, but might call one (which you therefore must define in some other compilation unit. The one-definition rule applies, though; the out-of-line version must have the same code as the inline offered here, in case the compiler calls that instead.
static inline will not generate a externally visible out-of-line version, though it might generate a file static one. The one-definition rule does not apply, since there is never an emitted external symbol nor a call to one.
C99 (or GNU99):
inline: like GNU89 "extern inline"; no externally visible function is emitted, but one might be called and so must exist
extern inline: like GNU89 "inline": externally visible code is emitted, so at most one translation unit can use this.
static inline: like GNU89 "static inline". This is the only portable one between gnu89 and c99
C++:
A function that is inline anywhere must be inline everywhere, with the same definition. The compiler/linker will sort out multiple instances of the symbol. There is no definition of static inline or extern inline, though many compilers have them (typically following the gnu89 model).

static inline:

 static 我们都知道当修饰函数的时候代表这个函数在当前文件是唯一的不可以extern对函数进行引用,限定了函数的访问空间
  1. 当被 static inline一起修饰,如果程序中所有对内联函数的调用都被替换在调用者的代码中,并且程序中没有引用过该函数地址,那么汇编代码就不会生成在text字段中。由于某些原因,一些对内联函数的调用不能被集成到函数中,内联函数定义之前调用的语句就不会被替换集成,也就是前面声明最后实现,这种情况发生内联函数的调用就无法集成,或者那些内联失败的情况下,内联函数就会正常编译为汇编代码,一个不使用 static 编译的内联总是会生成代码
  2. 如果你的inline想被别的.c调用就要在.h进行实现,由于全局函数不能重名,不加static就会报错多个地方定义,加了static就可以在对应.c作用域内使用,并且整体编译的时候,编译器就会把这些函数进行插入替换
  3. static inline在.c里面实现就是和1中说的一样

看一个例子:

//test_inline.c
#include <stdio.h>
#include "caculMax.h"

int main() {
    int value = getMax(4, 5);
    printf("value is %d\n", value);
    return 0;
}
//caculMax.h
#ifndef __CACULMAX_H_
#define __CACULMAX_H_
static inline int getMax(int left, int right) {
        return left > right ? left : right;
}
#endif

汇编编译:

#include <stdio.h>
#include "caculMax.h"

int main() {
 66a:	48 83 ec 08          	sub    $0x8,%rsp
}

__fortify_function int
printf (const char *__restrict __fmt, ...)
{
  return __printf_chk (__USE_FORTIFY_LEVEL - 1, __fmt, __va_arg_pack ());
 66e:	ba 05 00 00 00       	mov    $0x5,%edx
 673:	48 8d 35 aa 00 00 00 	lea    0xaa(%rip),%rsi        # 724 <_IO_stdin_used+0x4>
 67a:	bf 01 00 00 00       	mov    $0x1,%edi
 67f:	b8 00 00 00 00       	mov    $0x0,%eax
 684:	e8 b7 fe ff ff       	callq  540 <__printf_chk@plt>
    int value = getMax(4, 5);
    printf("value is %d\n", value);
    return 0;
}
 689:	b8 00 00 00 00       	mov    $0x0,%eax
 68e:	48 83 c4 08          	add    $0x8,%rsp
 692:	c3                   	retq   
 693:	66 2e 0f 1f 84 00 00 	nopw   %cs:0x0(%rax,%rax,1)
 69a:	00 00 00 
 69d:	0f 1f 00             	nopl   (%rax)

直接被替换为了结果,汇编中没有对应的内联代码记录了,的
在头文件中,如果你是在c++ 中可以使用namespace来进行 空间限定,不会报名称多定义错误

extern inline

extern 我们常见的做法就是声明变量或者函数,直接使用,等到链接器最终链接到一起进行使用

  1. 作用就类同一个宏定义, extern inline定义的函数不会生成具体的汇编代码

  2. 那么当你的程序中有些函数使用的内联函数地址或者可能汇编被拒绝,针对这种情况你需要定义一个函数体相同的非汇编代码

    Linux使用 string.h中

    extern inline char * strncpy(char * dest,const char *src,int count)
    {
    __asm__("cld\n"
    	"1:\tdecl %2\n\t"
    	"js 2f\n\t"
    	"lodsb\n\t"
    	"stosb\n\t"
    	"testb %%al,%%al\n\t"
    	"jne 1b\n\t"
    	"rep\n\t"
    	"stosb\n"
    	"2:"
    	::"S" (src),"D" (dest),"c" (count):"si","di","ax","cx");
    return dest;
    }
    

    在string.c中:

    #ifndef __GNUC__
    #error I want gcc!
    #endif
    
    #define extern
    #define inline
    #define __LIBRARY__
    #include <string.h>
    

    消除了extern和inline,这样最终的汇编代码也是有内联函数的吗,即使取函数地址也还是可以找到对应代码

例子:

//test_inline.c
#include <stdio.h>
#include "caculMax.h"

int main() {
    int value = getMax(4, 5);
    printf("value is %d\n", value);
    return 0;
}
//caculMax.h
#ifndef __CACULMAX_H_
#define __CACULMAX_H_
extern inline int getMax(int left, int right) {
        return left > right ? left : right;
}
#endif
000000000000066a <main>:
#include <stdio.h>
#include "caculMax.h"
int main() {
 66a:	48 83 ec 08          	sub    $0x8,%rsp
}

__fortify_function int
printf (const char *__restrict __fmt, ...)
{
  return __printf_chk (__USE_FORTIFY_LEVEL - 1, __fmt, __va_arg_pack ());
 66e:	ba 05 00 00 00       	mov    $0x5,%edx
 673:	48 8d 35 aa 00 00 00 	lea    0xaa(%rip),%rsi        # 724 <_IO_stdin_used+0x4>
 67a:	bf 01 00 00 00       	mov    $0x1,%edi
 67f:	b8 00 00 00 00       	mov    $0x0,%eax
 684:	e8 b7 fe ff ff       	callq  540 <__printf_chk@plt>
    int value = getMax(4, 5);
    printf("value is %d\n", value);
    return 0;
}
 689:	b8 00 00 00 00       	mov    $0x0,%eax
 68e:	48 83 c4 08          	add    $0x8,%rsp
 692:	c3                   	retq   
 693:	66 2e 0f 1f 84 00 00 	nopw   %cs:0x0(%rax,%rax,1)
 69a:	00 00 00 
 69d:	0f 1f 00             	nopl   (%rax)

实现内联嵌入,并且具体的内联函数没有出现在汇编中,那么如果我们取地址呢?

//test_inline.c
#include <stdio.h>
#include "caculMax.h"

int main() {
    int value = getMax(4, 5);
    printf("value is %d\n", value);
    return 0;
}
//caculMax.h
#ifndef __CACULMAX_H_
#define __CACULMAX_H_
extern inline int getMax(int left, int right) {
        return left > right ? left : right;
}
#endif

编译:

gcc -Og -g -std=gnu89 test_inline.c -o test_inline

结果:

/tmp/ccwVHoij.o: In function printf': /usr/include/x86_64-linux-gnu/bits/stdio2.h:104: undefined reference to getMax'
collect2: error: ld returned 1 exit status

未定义的引用

我们添加一个caculMax.c

#define inline  
#define extern
#include "caculMax.h"

编译:

gcc -Og -g -std=gnu89 test_inline.c caculMax.c -o test_inline

通过

可以自行查看汇编,发现生成了汇编代码
可能使用extern inline 会出现重复定义的错误,这个就是编译便准的问题了
参见这篇文章

两者区别

static inline最常用,编译器的对应优化也会去选择是否生成函数体,一般inline都加static,想多个文件调用就放在.h中,不会发生重定义,编译器最终会链接到一起

extern inline最终也是进行内联替换,但是使用是extern 链接的方式,尽量不使用

7. 内联失败的可能原因

  1. 没有开启-O优化

  2. 使用了可变参数

  3. 内存分配函数malloc

  4. 可变长度数据类型变量

  5. 非局部goto

  6. 递归函数

  7. 内联的函数不能有循环语句、

  8. 不能对内联函数地址

  9. 函数内部不能太复杂,不能存在过多条件判断

举个例子:

内存分配函数malloc

#include <stdio.h>
static inline int strlen1(const char* str) {
        return str ? (*str == '\0') ? 0 : 1 + strlen1(str + 1) : 0;
}

int main() {
    int value = strlen1("haha");
    printf("value is %d\n", value);
    return 0;
}

gcc -Og -g -std=gnu89 test.c -o test

查看汇编

//test.c
#include <stdio.h>
static inline int strlen1(const char* str) {
	return str ? (*str == '\0') ? 0 : 1 + strlen1(str + 1) : 0;
 66a:	48 85 ff             	test   %rdi,%rdi
 66d:	74 42                	je     6b1 <strlen1+0x47>
 66f:	80 3f 00             	cmpb   $0x0,(%rdi)
 672:	75 06                	jne    67a <strlen1+0x10>
 674:	b8 00 00 00 00       	mov    $0x0,%eax
 679:	c3                   	retq   
 67a:	48 89 f8             	mov    %rdi,%rax
 67d:	48 83 c0 01          	add    $0x1,%rax
 681:	74 27                	je     6aa <strlen1+0x40>
 683:	80 7f 01 00          	cmpb   $0x0,0x1(%rdi)
 687:	75 09                	jne    692 <strlen1+0x28>
 689:	b8 00 00 00 00       	mov    $0x0,%eax
 68e:	83 c0 01             	add    $0x1,%eax
}
 691:	c3                   	retq   
static inline int strlen1(const char* str) {
 692:	48 83 ec 08          	sub    $0x8,%rsp
	return str ? (*str == '\0') ? 0 : 1 + strlen1(str + 1) : 0;
 696:	48 8d 78 01          	lea    0x1(%rax),%rdi
 69a:	e8 cb ff ff ff       	callq  66a <strlen1>
 69f:	83 c0 01             	add    $0x1,%eax
 6a2:	83 c0 01             	add    $0x1,%eax
}
 6a5:	48 83 c4 08          	add    $0x8,%rsp
 6a9:	c3                   	retq   
	return str ? (*str == '\0') ? 0 : 1 + strlen1(str + 1) : 0;
 6aa:	b8 00 00 00 00       	mov    $0x0,%eax
 6af:	eb dd                	jmp    68e <strlen1+0x24>
 6b1:	b8 00 00 00 00       	mov    $0x0,%eax
 6b6:	c3                   	retq   

00000000000006b7 <main>:

int main() {
 6b7:	48 83 ec 08          	sub    $0x8,%rsp
	return str ? (*str == '\0') ? 0 : 1 + strlen1(str + 1) : 0;
 6bb:	48 8d 3d b3 00 00 00 	lea    0xb3(%rip),%rdi        # 775 <_IO_stdin_used+0x5>
 6c2:	e8 a3 ff ff ff       	callq  66a <strlen1>
 6c7:	8d 50 01             	lea    0x1(%rax),%edx
}

__fortify_function int

多次的strlen1调用,肯定不是内联了

我们可以加上-Winline让gcc对标志成inline但不能替换的函数给出警告,以及原因

gcc -Og -g -std=gnu89 -Winline test.c -o test

test.c: In function ‘main’:
test.c:3:19: warning: inlining failed in call to ‘strlen1’: call is unlikely and code size would grow [-Winline]
 static inline int strlen1(const char* str) {
                   ^~~~~~~
test.c:4:40: note: called from here
  return str ? (*str == '\0') ? 0 : 1 + strlen1(str + 1) : 0;
                                        ^~~~~~~~~~~~~~~~

一目了然

10.内联和宏定义的区别

内联是还拥有函数的参数检查和类型判断,宏,仅仅是文本替换,我们都知道宏是容易出问题的,看一个简单的代码
例子
#include <iostream>
#define exa(b, c)    b + c
inline int Exa(int b, int c) {
    return b + c;
}
void sum(int a) {
    std::cout << a * exa(2, 3) << std::endl; //结果 7 , 实际运算为 a * b + c
    std::cout << a * Exa(2, 3) << std::endl; //结果 10
}
int main() {
    sum(2);
}

总结

  1. 当你想使用宏定义函数的时候首先考虑内联

  2. 当你想封装一个简短的函数,考虑内联

  3. 不同的编译标准对inline的效果不同

  4. 使用内联代替去除函数和运算宏,使用const代替变量宏

  5. 好处:内联的函数,有函数的基础功能,检查类型等,没有开销,直接插入到调用处

  6. 坏处:对待嵌入式这种视存储珍贵的,内联就需要仔细考虑,类似那种用空间换时间的感觉,
    内联让函数体变得臃肿,需要编译器支持

参考

extern inline 重定义
gnu的优化标准
Linux内核注释0.11

posted @ 2020-12-08 22:57  make_wheels  阅读(1598)  评论(0编辑  收藏  举报