- 条件内联:编译,调试和配置等过程与内联存在一定的冲突,因此做这些工作时,都希望将内联决策推迟到开发周期的后期。
思路:利用编译行参数向编译器传递一个宏定义。
输入参数用来定义名为INLINE的宏,外联方法包含于标准的.c文件,内联的方法放置在.inl文件中,如果需要对.inl中的方法内联,可以在编译命令中使用-D选项来定义INLINE宏。
编译时内联开关示例:
文件x.h
if !defined(_X_H)
#define _X_H
class X {
int y (int a);
};
#if defined(INLINE)
#include x.inl
#endif
#endif //_X_H
文件x.inl
#if !defined(INLINE)
#define inline
#endif
inline
int X::y (int a) {
...
}
文件x.c
#if !defined(INLINE)
#includex.inl
#endif
.h文件如一贯所示。如果INLINE被定义,则.h文件将包含.inl文件,且各个方法之前的inline指示符将不受影响、若为定义INLINE,.h文件将不会包含内联方法,这些方法会包含在.c文件中,同时每个方法前面的inline指示符将被清除。
- 选择性内联:针对一个方法,某些地方内联,某些位置正常调用。
可以写两个版本直接解决:
文件x.h
class X {
int inline_y (int a);
int y (int a);
};
#include x.inl
文件x.inl
inline
int X::y (int a) {
... //y的原始实现
}
文件x.c
int X::y (int a) {
return inline_y(a);
}
通过以上代码,获得两种版本的y。外联方法y()中的return inline_y(a);存在好处:对于方法内的任何静态变量,单个方法体可以为其产生唯一实例。
- 递归内联:直接递归是无法内联的。尾部递归是递归方法的一种,他变现为方法在达到它的基线之前一直递归下降。
当达到基线条件后会执行一些操作并终止方法。典型的二叉搜索树就是一个很好的尾部递归的例子。
binary_tree* binary_tree::find(int key){
if(key == id){
return this;
}
else if(key > id){
if(right) return right.find(key);
}
else {
if(left) return left.find(key);
}
return 0;
}
正如所见,当程序满足基线后,除返回一个指向对象的指针外,没有执行任何操作。因为递归生成的调用堆栈的上下文微不足道。
实际上,如果编译器能够简单的预留一个变量以保存this,那么在不生成新方法上下文的情况下,该方法就可以被执行。
binary_tree* binary_tree::find(int key){
binary_tree *temp = this;
while(temp){
if(key == temp->id){
return this;
}
else if(key > temp->id){
temp = right;
}
else {
temp = left;
}
}
return 0;
}
还有一种针对递归最为普遍的方法:将该递归方法重新展开一次并命名,然后内联该新方法,新方法又会调用老方法,例,二叉树中生成id的中缀列表的方法:
void binary_tree::key_out(){
if(left) left->key_out();
cout << id << endl;
if(right) right->key_out();
}
使用内联把key_out方法按常用方式展开一次,这样导致代码版本为原先2倍,速度比原始版本快2~3倍:
inline
void binary_tree::UNROLLED_key_out(){
if(left) left->key_out();
cout<< id << endl;
if(right) right->key_out();
}
void binary_tree::key_out(){
if(left) left->UNROLLED_key_out();
cout << id << endl;
if(right) right->UNROLLED_key_out();
}
单层展开提供了最佳性价比,然而如果必要,可以进一步展开,如4次迭代等
使用旧式的C #define 宏扩展可以将展开的方法统一起来。
#define KEY_OUT_MACRO(inline_arg, my_label, call_label) \
\
inline_arg \
void binary_tree::UNROLLED##my_label##_key_out(){ \
if(left) left->UNROLLED_key_out##call_label##_key_out(); \
cout << id << endl; \
if(right) right->UNROLLED_key_out##call_label##_key_out(); \
}
KEY_OUT_MACRO(inline, 3, 0)
KEY_OUT_MACRO(inline, 2, 3)
KEY_OUT_MACRO(inline, 1, 2)
KEY_OUT_MACRO(\\t, 0, 1)
inline
void x::key_out(){
UNROLLED_key_out();
}
尽管这些代码看起来较少,但是当C++的预处理器完成宏扩展、编译器完成内敛之后,就会像之前一样产生代码膨胀。
因此,推广宏扩展这种脆弱的机制是很困难的,然而当需要采取这种极端措施时,其优点(只有递归一个版本)通常会盖过缺点。
与4层展开相比,8层展开所得性能会提升2-3倍,然而4层展开代码大小是原始版本的4倍,8层展开就是原来的64倍。
注:
3.1. #的功能是将其后面的宏参数进行字符串化操作(Stringizing operator),简单说就是在它引用的宏变量的左右各加上一个双引号。
例如#define STRING(x) #x
则:char *pChar = STRING(hello); 与char *pChar = "hello"; 一样
3.2. ##进行拼接,就是消除中间的内容
- 对静态局部变量的内联
部分编译器会允许内联静态变量,但是会出一个问题:错误地为这些内联变量创建多个实例。
对于内联包含静态变量的方法来说,问题是要结局静态变量的唯一性以及保证静态变量被初始化。
对于限定范围内,在逻辑上来说,局部静态变量就是全局变量。这需要连接器足够智能,检测到使用这种静态变量的各种情况,然后创建该变量,接着在全局数据空间内保留空间,进而为该变量创建初始化代码(或确保已经初始化),最后将所有对该变量的内联引用连接到全局数据区域为该变量新建的唯一实例中。(在相互分离的编译模块领域内,烬烬确定动向静态变量的问题就已经让不少编译有苦不堪言。)
判断编译器是否可以恰当的处理静态变量的内联,示例:
z.h:
inline
int test () {
static int i = 0;
return ++i;
}
y.cpp:
#include "z.h"
void test_a () {
int i = test();
cout << i;
}
x.cpp:
#include "z.h"
void test_b () {
int i = test();
cout << i;
}
main.cpp
int main () {
test_a();
test_b();
count << endl;
return 0;
}
如果编译器创建的代码正确,则会分别对三个.cpp文件进行编译及连接并产生一个可执行程序,输出结果为12。如果结果为11,表明编译器在内联含有静态变量的方法方面存在问题。(解决方法:类的内部之用静态成员变量即可)
总结:
a. 条件内联可以阻止内联的发生,这样可以减少编译时间,同时简化了开发前期的调试工作。
b. 选择性内联是一种只在某些地方内联方法的技术,他只在对性能有重大影响的路径上对方法调用进行内联
c. 内联的目标是消除调用开销。在使用内联之前请弄清楚系统中真正的调用代价。
(例如SPARC体系结构,拥有多寄存器集,其方法调用代价较小)