C++中inline函数最终造成了什么效果?

Tuesday, February 7, 2023
本文共1570字
4分钟阅读时长

⚠️本文是作者P3troL1er原创,首发于https://peterliuzhi.top/posts/c++%E4%B8%ADinline%E5%87%BD%E6%95%B0%E6%9C%80%E7%BB%88%E9%80%A0%E6%88%90%E4%BA%86%E4%BB%80%E4%B9%88%E6%95%88%E6%9E%9C/。商业转载请联系作者获得授权,非商业转载请注明出处!

Numberless are the worlds wonders, but none more wonderful than man. — Sophocles

引入

在C++中我们经常使用inline内联函数来代替宏,而宏是在预编译阶段进行简单的替换,那内联函数会产生什么效果呢?

我写了一个简单的小程序来验证这个问题:

#include <stdio.h>
#include <string>
#define ONE 1

inline char _IndexFromEnd(std::string& num_str, char position) {
	// 如果越界,返回'0',在StringNumAddOrSub中计算会得到0
	if (position >= num_str.length()) return '0';
	// 从后往前数
	return num_str.at((num_str.length() - 1) - position);
}

inline char get_one(){
    return ONE;
}
int	main(int argc, char **argv)
{
    std::string num = "123456";
    char a = _IndexFromEnd(num, 1);
    char b = _IndexFromEnd(num, 100);
    char c = get_one();
    putchar(a);
    putchar(' ');
    putchar(b);
    putchar(' ');
    putchar(c);
    return 0;
}

然后得到了预编译结果、普通编译链接结果、优化编译链接结果:

文内图片

查看预编译结果

预编译器将include的头文件简单地复制到了我们代码中include的位置,因此最后我们得到的预编译结果中,我们自己写的代码位于六万多行的位置:

文内图片

可以看到,我们define的宏ONE已经被简单地替换掉了,但是inline函数并没有进行替换

查看普通编译链接结果

我们使用ida进行反编译后可以看到这里内联函数并没有嵌入到主函数之中,而是像一个普通的函数一样被调用

文内图片

文内图片

而且同样有传参的操作:

文内图片

文内图片

或许这就是内联函数能够被调试的原因——在调试版本的程序中,它和普通函数并没有什么区别

优化编译链接结果

优化后我们发现主函数已经变成了我们不认识的样子:

文内图片

而其中那个只是返回了1的内联函数直接消失了,变成了常量值被打印出来:

文内图片

而这里sub_1004016B0函数也不再是我们写的内联函数,而是变成了混合了basic_string中的一个函数(应该就是我们在内联函数中调用的at函数)的函数,或者说,它将at函数嵌入到了我们的函数中:

文内图片

而同时我们可以查看at的源代码:

文内图片

会发现这个函数的返回值是被constexpr修饰的,说明这是一个在编译器确定的常量

如果我们将get_one确定为constexpr返回值的函数,则它一定会在编译期进行计算,就算加上-g:

文内图片

与非内联函数对比

我们试着与非内联函数对比:

文内图片

在-g的时候并没有区别:

文内图片

然后我们再看下-O2的时候:

文内图片

这完全没差嘛!所以说如果你自己都不知道内联会怎么发生,就不要期望内联真正起效了。

如果我们将代码中的复杂返回逻辑改掉,我们会发现内联"发生"了:

文内图片

文内图片

这看上去好像没有发生内联,但是我们查看sub_1004016B0函数会发现,由于内联函数内调用了另一个函数,因此我们得到的最终结果还是一个函数。

但是由于优化开启,所以就算是普通函数也会变成这样,并没有什么差别。也就是说,这样的内联并无意义(因为不开启优化内联不会发生)

因此,不要在内联函数内进行嵌套调用(或者递归)

内联函数嵌套调用内联函数

那如果我嵌套调用的是一个内联函数呢,情况会不会发生变化?

文内图片

答案是确定的,优化开启的情况下,编译器直接计算出了结果:

文内图片

而在-g的情况下,编译器甚至连这种常量结果都没有计算出来:

文内图片

或许你会问,如果返回值不是一个字面常量呢?就像下面这样:

文内图片

显然内联发生了:

文内图片

因此,一个返回逻辑简单的内联函数调用另一个返回逻辑简单的内联函数在优化开启的情况下确实是会发生内联的

结论

因此,我们通过这些简略的事实能够得到一个粗糙的结论:

内联函数在编译器优化之前和普通函数并无差别,在优化之后一些具有较复杂返回逻辑的内联函数也无法内联。同时就算返回逻辑简单,也要避免嵌套非内联函数,因为这在优化开启的情况下并无意义。

因此,如果函数返回的值是一个在编译期能够确定的常值,尽量使用constexpr修饰返回值;不要嵌套非内联函数,不要复杂化内联函数的返回逻辑

可见,内联函数的实现较为复杂,如果期望通过内联函数优化程序,一定要谨慎行事,因为虽然内联函数至少不会让程序性能降低,但是会给代码阅读者造成困惑并将他们的思路引导向错误的方向,某种意义上来说,它让代码文字的“效率”降低了

✍️作者还是C++初学者,因对内联函数的行为有所疑惑才试图用自己笨拙的方式探寻真相,如果错漏,敬请见谅🙏