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 函数, 有两个阶段:

  1. 分析函数定义, 以决定函数的 intrinsic inline ability(本质的 inline 能力). "intrinsic"(本质的, 固有的) 在这里指与编译其相关.
    如果函数因复杂度, 或其建构问题, 被判断不可成为 inline, 它会被转为一个 static 函数, 并在被编译模块内产生对应的函数定义。 在一个支持模块个别编译的环境中, 编译器几乎没有什么权宜之计。理想情况下, 链接器会将被产生出来的重复东西清理掉, 然而一般来说, 目前市场上的链接器并不会将该调用产生的重复调试信息清理掉. UNIX 环境中的 strip 命令可达到这个目的。
  2. 真正的 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 函数比起来, 需要更小心处理.

posted @ 2021-04-22 11:38  点|滴  阅读(14)  评论(0)    收藏  举报