在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
作者:刘俊延(Alinshans)本系列文章针对我在写C++代码的过程中,尤其是做自己的项目时,踩过的各种坑。以此作为给自己的警惕。
博客园的编译好渣,我用 Markdown 重写了一遍,内容也作了修正和调整,请移步:https://alinshans.github.io/2017/05/23/p1705231/
第一次修改:2017/3/26 发表于 : 2017/3/15
今天讲一个小点,虽然小,但如果没有真正理解它,没有真正熟悉它的里里外外,是很容易出错的 —— inline。 关于一些简单的介绍和使用,可以先看我 这篇笔记 。接下来进入正题。
一、如何使用 inline? 你知道,inline 函数可以减小函数调用的开销,你可能会想,嗯,我这个函数那么短,我把它声明为 inline,可以提高程序运行的效率!考虑这样一个例子: // A.h // A.cc // main.cc #include "A.h" int main() { A a; a.foo(1); a.bar(1); } 首先,你知道,①inline 需要看到函数实体,所以要跟定义放在一起。于是你想在 A.cc 中在为 foo 的定义加上一个 inline : inline void A::foo(int i) 然后开心的编译运行,WTF!!!编译器居然报错了?!!不就加了个 inline 吗!仔细观察编译器给的出错信息,如果你用的是VS,那么你大概会看到这样的信息: error LNK2019: 无法解析的外部符号……如果你用的是GCC,你会发现当你使用 g++ -c main.cc 时(即编译),是不会产生任何错误的,然后当你使用 g++ main.o -o a.out 时(即链接),就报错了。说明,这是链接的时候出错了。在这里要说明一下,大多数的建置环境都是在编译过程进行 inlining(为了替换函数调用,编译器需要知道函数的实体长什么样,这解释了①),某些可以在连接期完成,少数的可以在运行期完成。我们只考虑绝大部分情况:Inlining 在大多数C++程序中是编译期行为。 大部分函数默认的就是外部链接,也就是外部可以访问,而 inline 函数默认具有内部链接,也就是对本文件可见,对其它文件不可见。那么自然我们在 main.cc 中调用它,没法看到它的定义,于是就出现了连接错误。OK,你学到了 ②一般 inline 需要放在头文件中。 首先你要先了解一下内部链接与外部链接,可以看这里。它提到:
ok,类的成员函数是具有外部链接的,然后我们看这里,它提到:
嗯,意思很明白了,就是如果一个函数,是外部链接的,你给它搞成 inline 了,那么,请你在每一个编译单元都做一个 inline 定义。也就是说,如果你想让上面的代码运行,没问题,那请把 main.cpp 改成这样: // main.cpp #include <iostream> #include "A.h" class A; inline void A::foo(int i) { std::printf("%d\n", i); } int main() { A a; a.foo(1); } 我想,如果有十个编译单元要引用它呢?一百个呢?你可能不愿意这样写。而在这里开头还有提到:
在类内定义的成员函数,是自动 inline 的,不需要你去加,LLVM CodingStandards 也是这样提出的。 那你可能会想马上想到还有一种情况:如果一个类成员函数,既不定义在类内,也不定义在编译单元,而是定义在头文件,并且在类外,这种情况,又会发生什么呢?也就是这样: // A.h #include <cstdio> class A { public: void foo(int i); void bar(int i); }; inline void A::bar(int i) { std::printf("%d", i + 1); } 嗯,可以,这样写通过编译,并且可以运行了。不过,它如你所想提高效率了吗?我们可以探究一下。在vs下可以用调试看反汇编,现在用GCC分别运行以下命令: g++ -E main.cc -o main.i g++ -S main.i -o main.s g++ -O2 -S main.i -o main2.s 我们来看一下 main.s 中的主要部分: call ___main leal -9(%ebp), %eax movl $1, (%esp) movl %eax, %ecx call __ZN1A3fooEi subl $4, %esp leal -9(%ebp), %eax movl $1, (%esp) movl %eax, %ecx call __ZN1A3barEi subl $4, %esp movl $0, %eax movl -4(%ebp), %ecx 我们再看一下 main2.s 中的这个部分: call ___main leal -9(%ebp), %ecx movl $1, (%esp) call __ZN1A3fooEi subl $4, %esp movl $2, 4(%esp) movl $LC0, (%esp) call _printf movl -4(%ebp), %ecx 在不开优化的情况下,程序诚实的执行,开O2优化的情况下,我们已经看不到 bar 函数的调用了。不过这真的是拜你加的 inline 所赐的吗?为了验证,我们去掉 inline,打算再次重复上面的过程,然后你就会发现,WTF!!!编译器又报错了??发生了什么??
二、什么时候应该使用 inline? 嗯,终于我们来到了第二个问题,我们发现,当我们给函数去掉 inline 时,居然无法通过编译了!它给出来的错误信息是:重定义的符号。让我们冷静下来,想一想,然后你就会恍然大悟:一个函数可以有多次声明,但只能有一次定义,而我们定义在 A.h 的 bar 函数的定义,被 A.cc 和 main.cc 都包含了一遍!所以就出现了重定义的错误!是的是的,我也想不到有什么理由让一个类成员函数的定义即不出现在类内部,也不出现在编译单元,除非是模板类成员函数/类模板成员函数。不过你现在应该对 inline 与类成员函数的种种事情,有了非常清晰的认识了。即 ②不要把 inline 用在类的成员函数上。当然,也别写出上面那种情况的代码。 然后我们来看看 inline 跟普通函数结合的情况。这种情况,更容易被我们忽视,例如,我们想在 A.h 中加一个函数: // A.h 它很短,要不要使用 inline 呢?经过刚刚的问题,你应该会谨慎的想到,这里,要使用 inline ,如果不使用,就会出错。原因跟上面提到的是一样的。当然,它不是非得使用 inline 不可,你可以把它的函数定义放在源文件,就不会有重复定义的问题。甚至你也可以在头文件定义并且使用 static 修饰它,也可以解决问题,这个就不展开了。 但当你用上模板时,情况发生了改变。若你把这个 max 函数改成一个模板函数: template <typename T> T max(const T& a, const T& b) { return a < b ? b : a; } 这个时候,无论你有没有使用 inline,它都是可以运行的。这是因为,模板是具有“内联”语义的。所以,类模板,函数模板,类函数模板,都不需要加 inline 。回到正题,什么时候可以使用 inline 呢?③使用 inline 当且仅当函数的定义在头文件中并且被多个源文件包含时。
三、inline 可以提升程序运行效率? 我们刚刚还没有完成我们的实验,我还没有打消你使用 inline 去“优化”程序的念头。所以,让我们再次做一次实验,这次为了方便,我在 main.cc 定义一个函数:
// main.cc #include <cstdio> int test(int i) { i = i + 1; return i; } int main() { std::printf("%d\n", test(1)); }
这样的短代码,你想优化了是吧?先别急,我们就这样,编译汇编看看,运行同样的命令: g++ -E main.cc -o main.i g++ -S main.i -o main.s g++ -O2 -S main.i -o main2.s 然后看 main.s (未开优化)的主要部分: call ___main movl $1, (%esp) call __Z4testi movl %eax, 4(%esp) movl $LC0, (%esp) call _printf 然后再看看 main2.s(开O2优化)的这个部分: call ___main movl $2, 4(%esp) movl $LC1, (%esp) call _printf 嗯是的没错,你没有声明 inline,但是编译器的优化,帮你把这个函数内联展开了,所以在 main2.s 中看不到 test 的调用了。你加上 inline 重复这个过程,还是会得到一样的结果。还没有死心吗?你是不是想说,这个函数太简单了,我是编译器我都看得出来可以优化啊!听说复杂一点编译器就不会优化了!比如函数里面有循环,递归什么的! 好,于是你改成这样: // main.cc #include <cstdio> int test(int i) { int x = 0; for (int j = 0; j < i; ++j) { x += j; } return x; } int main() { std::printf("%d\n", test(100)); } 再次编译汇编,你猜猜你会看到什么?好吧,我只把 main2.s 中的那个部分给你看看: call ___main movl $4950, 4(%esp) movl $LC1, (%esp) call _printf 你还想说什么吗?如果还没死心,请继续尝试其他情况。我不会帮你试,不过我可以帮你试试这个情况: // main.cc #include <cstdio> #include <cmath> inline int test(int i) { int prime[100]; int k = 0; for (int n = 2; n <= i; ++n) { bool is_prime = true; for (int j = 2; j <= static_cast<int>(std::sqrt(n)); ++j) { if (n % j == 0) { is_prime = false; break; } } if (is_prime) { prime[k] = n; ++k; } } int sum = 0; for (int n = 0; n < k; ++n) { sum += prime[n]; } return sum; } int main() { std::printf("%d\n", test(100)); } 嗯。。长是长了点,但是你声明了一个 inline 呀!好吧,我们再看看生成的两份汇编代码: main.s: call ___main movl $100, (%esp) call __Z4testi movl %eax, 4(%esp) movl $LC1, (%esp) call _printf movl $0, %eax main2.s: call ___main movl $100, (%esp) call __Z4testi movl $LC2, (%esp) movl %eax, 4(%esp) call _printf xorl %eax, %eax 这一次,无论是否开优化,都调用了 test。然后你很无奈的发现,编译器是否选择内联,跟你声不声明没有半毛钱关系啊!!
四、 inline 的真正意义? 现在你该好好的思考,什么是 inline,是内联吗?inline 的意义是什么,是发起一个内联请求吗? 你认为加 inline 是为了提高程序的运行效率,但是事实上,并不会跟 inline 有什么关系啊。但有的时候,你不加 inline,却会出错。这跟“内联”两个字,好像已经没什么关系了? 好好的思考一下吧。
这么快就往下看了,花点时间在思考一下?
好吧。 inline,跟 static , extern 一样,都是链接指令,它在很久很久以前,是作为给编译器优化的提示符。而 inline 的含义是非绑定的,编译器可以自由的选择、决定是否 inline 一个函数。如今,编译器根本不需要这样的提示,如果它认为一个函数值得 inline,它会自动 inline,否则,即使你 inline 了,它也会拒绝。如果你仔细阅读 http://en.cppreference.com/w/cpp/language/inline 的话,尤其是其中的 Desription:
你会发现,全文几乎没有提到 “优化代码”、“减小开销” 等等字眼。而你在网上所搜素到的关于 inline 的信息,几乎都告诉你,inline 可以怎么怎么优化。要么用了假的搜索引擎,要么看了假网页 ,要么…… 在这篇 SO 中,有一段话:
看完你应该差不多能理解了。现在的编译器,并不需要你用 inline 提醒,所以,当且仅当你认为使用 inline 会加快程序运行效率时,不要使用 inline 。inline 这个关键字,在C++里就是一个骗局。它真正的意义并不是去内联一个函数,而是表示 别怕!无论你看到了多少个定义,但实体就我一个! 在 Reference 中有有这样一句话:
翻译过来就是 ④ inline 的含义更多的是 “允许多重定义” 而不是 “优先选择内联函数” 。全文基于 C++17 及以前的讨论。
五、总结
1、inline 需要看到函数实体,所以要跟定义放在一起 2、不要把 inline 用在类的成员函数上 3、使用 inline 当且仅当函数的定义在头文件中并且被多个源文件包含时 4、inline 的含义更多的是 “允许多重定义” 而不是 “优先选择内联函数” 5、模板不需要声明 inline,也具有 inline 的语义
※注:以上总结适用于不熟悉、不了解 inline 的同学。若对以上内容都了解,使用 inline 的时候,很明白很清楚在做什么,会发生什么,那就随便怎么用啦!
|
2023-10-27
2022-08-15
2022-08-17
2022-09-23
2022-08-13
请发表评论