4.5 Inline Functions
C++当中定义内联函数,可以让编译器将对内联函数的调用直接展开。
https://blog.csdn.net/vistas_fh/article/details/70983880
这就多少有点像宏定义了,而且没有宏定义的缺点(预处理替换,无法当成变量链接到符号表、调用有可能导致参数异常被改、等等)。
使用内联函数可以避免函数调用的开销(栈开辟、返回地址设定、栈展开),在一定的程度上可以提高程序的性能。
编译器将函数展开,会直接导致可执行程序变大。(导致运行缺页、cache命中率降低)
编译器在某些情况下会禁止inline:
1:虚函数
inline展开是在编译时进行的(宏定义是在预处理时展开的),而虚函数是在运行时决定调用哪个函数的。因此编译器对虚函数的inline无能为力
2:带有循环或者递归的调用
如何inline函数过于复杂,复杂到函数本身执行的成本,比函数调用(栈开销)成本还要高,禁止inline
3: 通过函数指针调用inline函数
虽然编译器会展开inline函数,但还是会生成函数体。通过函数指针,使得其等于内联函数,通过函数指针调用,无法inline
inline void f() {...}
void (*p)()=f;
p(); //此处无法内联
下面是 Point class 的一个加法运算符的可能实现内容:
class Point
{
friend Point operator+(const Point&, const Point&);
};
Point operator+(const Point &lhs, const Point &rhs)
{
Point newPoint;
newPt._x = lhs._x + rhs_x;
newPt._y = lhs._y + rhs_y;
}
理论上, 一个比较干净的做法是使用 inline 函数完成 set 和 get 函数:
//void Point::X(float x){ _x = x; }
//float Point::x(){ return _x; }
newPt.X()(lhs.x() + rhs.x());
由于受限只能在上述两个函数中对 x 直接存取, 因此也就将稍后可能发生的 data membrs 的改变(例如在继承体系中的上移下移) 所带来的冲击最小化. 如果把这些函数声明为 inline, 就可以继续保持直接存取 memebers 的那种高效率--亦兼顾了函数的封装性. 此外, 加法运算符不再需要被声明为 friend.
实际上不能把任何函数都声明为 inline。关键词 inline(class declaration 中的 member function 或 friend function 的定义) 只是一项请求, 如果这项请求被接受, 编译器就必须认为它可以用一个表达式将这个函数合理的扩展开来。在某个层面上, 其执行成本比一般的函数调用及返回机制所带来的负荷低。
一般处理一个 inline 函数, 有两个阶段:
- 分析函数定义, 以决定函数的 intrinsic inline ability(本质的 inline 能力). "intrinsic"(本质的, 固有的) 在这里指与编译其相关.
如果函数因复杂度, 或其建构问题, 被判断不可成为 inline, 它会被转为一个 static 函数, 并在被编译模块内产生对应的函数定义。 在一个支持模块个别编译的环境中, 编译器几乎没有什么权宜之计。理想情况下, 链接器会将被产生出来的重复东西清理掉, 然而一般来说, 目前市场上的链接器并不会将该调用产生的重复调试信息清理掉. UNIX 环境中的 strip 命令可达到这个目的。 - 真正的 inline 函数扩展操作是在调用的那个点上, 这会带来参数的求值操作, 以及临时性对象的管理.
同样是在扩展点上, 编译器将决定这个调用是否不可为 inline. 在 cfront 中, inline 函数如果只有一个表达式, 则其第二或后继的调用操作:
newPt.x( lhs.x() + rhs.x() );
就不会扩展开来,因为在 cfront 中, 它被变成:
//虚拟 C++ 码, 建议的 inline 扩展形式
newPt._x = lhs._x + x__5PointFV( &rhs ); // 成员函数->非成员函数
这就完全没有效率上的改善! 对此, 能做的就是重写其内容:
newPt.X(lhs._x + rhs._x);
形式参数
在 inline 扩展期间, 到底发生了什么? 每一个形式参数被对应的实际参数取代。
副作用就是不可以只是简单的替换程序中出现的每一个形式参数, 这将导致对实际参数的多次求值操作.
一般面对会带来副作用的实际参数, 通常都需要引入临时对象。换句话说, 如果实际参数是一个常量表达式, 可在替换之前完成求值操作; 如果既不是个常量表达式, 也不是个带有副作用的表达式, 那就直接代换, 如:
inline int Min(int i, int j)
{
return i < j ? i : j;
}
//下面是三个调用操作:
inline int Bar()
{
int minVal;
int val1 = 1024;
int val2 = 2048;
minVal = Min(val1, val2); //1)
minVal = Min(1024, 2048); //2)
minVal = Min(Foo(), Bar() + 1); //3)
return minVal;
}
1) 会被扩展为: minVal = val1 < val2 ? val1 : val2;
2) 直接拥抱常量: minVal = 1024;
3)则引发参数的副作用, 它需要导入一个临时对象, 以避免重复求值:
int t1;
int t2;
minVal = ( t1 = Foo() ), ( t2 = Bar() + 1), t1 < t2 ? t1 :t2;
局部变量(Local Variables)
如果在 inline 定义中加一个局部变量:
inline int min(int i, int j)
{
int minVal = i < j ? i : j; // 局部变量
return minVal;
}
这个局部变量需要什么额外处理吗? 如果有以下的调用操作:
{
int localVal;
int minVal;
minVal = min(val1, val2);
}
inline 被展开后, 为了维护其局部变量, 可能会成为这个样子(理论上例子中的局部变量可以被优化, 其值可以直接在 min() 中计算):
{
int loaclVar;
int minVal;
//将 inline 函数的局部变量处以 mangling 操作
int _min_lv_minVal;
minVal = (_min_lv_minVal = val1 < val2 ? val1 : val2 ), _min_lv_minVal;
}
一般而言, inline 函数中的每一个局部变量都必须放在函数调用的一个封闭区段中,拥有一个独一无二的名称。如果 inline 函数以单一表达式扩展多次, 那么每次扩展都需要自己的一组局部变量. 如果 inline 函数以分离的多个式子被扩展多次, 那么只需要一组变量, 就可以重复使用(因为在一个封闭区段中, 有自己的 scope)。
inline 函数中的局部变量, 再加上有副作用的参数, 可能会导致大量临时性对象的产生, 特别是单一表达式被扩展多次的情况:
minVal = Min(val1, val2) + Min(Foo(), Foo() +1);
//为局部变量产生的临时变量
int __min_lv_minVal_0;
int __min_lv_minVal_1;
//为放置副作用值而产生的临时变量
int t1;
int t2;
minVal =
((_min_lv_minVal_0 = val1 < val2 ? val1 : val2), _min_lv_minVal_0)
+
((_min_lv_minVal_1 = (t1 = Foo()), (t2 = Foo() + 1), t1 < t2 ? t1 :t2),
_min_lv_minVal_1);
inline 函数对于封装提供了一种必要的支持, 可以有效存取封装于 class 中的 non-public 数据. 它同时也是 C 程序中大量使用的 #define 的一个安全替代品--特别是如果宏中的参数有副作用的话. 然而一个 inline 函数如果被调用太多次的话, 会产生大量的扩展码. 使程序的大小暴涨.
参数带有副作用, 或是以一个单一表达式做多重调用, 或是在 inline函数中有多个局部变量, 都会产生临时性变量, 编译器也许(也许不) 能够把它们移出.
此外, inline 中再有 inline, 可能会使一个表面上看起来平凡的 inline 却因为复杂度而没办法扩展开来.这种情况可能发生于复杂 class 体系下的 constructors, 或是 object 体系中一些表面并不正确的 inline 调用所组成的串链--它们每一个都会执行一小组运算, 然后对另一个对象发出请求. 对于既要安全又要效率的程序, inline 函数提供了一个强有力的工具, 然而, 与 non-inline 函数比起来, 需要更小心处理.

浙公网安备 33010602011771号