为什么不每个函数都用inline??

时间:2022-01-16 04:34:35
既然inline可以免去函数的调用,那为什么不每个函数都用inline呢??

37 个解决方案

#1


因为不是每一个函数都能在编译期在调用处展开,如虚函数就不能.

#2


因为对于大函数来说,inline展开会令代码长度增加,带来的消耗反而更大。

#3


那什么样的函数才算了大函数呢?

#4


空间/时间的权衡问题吧

#5


不嫌打字麻烦,也不嫌编译器报一堆warning的情况下可以使用,但是记得一旦用了inline,那么函数实现必须与函数声明在同一个文件中,不能一个在.h,一个在.cpp

#6


那么函数实现必须与函数声明在同一个文件中,不能一个在.h,一个在.cpp
???

#7


inline是用于实现的,用于.cpp才有可能是内联的。声明的时候完全不需要的。
在林锐的《高质量C++编程指南》里面有详细的介绍

#8


既然inline可以免去函数的调用,那为什么不每个函数都用inline呢??
~~~~~~~~~~~~~~~
inline函数可以免去函数调用的损失,但是却大大增加了代码量(代码膨胀)
如果对所有函数使用inline,不可避免的是,一个编译文件将是是一个超大规模的
文件(所有函数都inline了),对于上大的工程文件,他的编译时间是不可忍受的!

inline对编译器只是一个提示说明,编译器可以对你的函数inline也可以不inline。
所以即使你的函数都写上inline,最后的结果,也可能一个inline也没有。如果有些函数确实
想采用inline,但是又怕编译给忽略掉,可以采用__inline,并在编译时刻选择
以__inline方式内联而不是默认方式内联就可。

#9


空间/时间的权衡问题吧
------------
同意

#10


那什么样的函数才算了大函数呢?
--------------------------------

一般函数体中含有循环,多个判断,或超过十几行的话,就算大函数,其调用开销是执行开销的高阶无穷小,可以忽略不计了.

#11


不行的
因为有些老的编译器如果把不能内联的函数内联会有问题的

#12


内联函数的执行速度比一般函数快,但是代价却是增加内存。
如果一个程序在5个地方调用同一个内联函数,相当于该程序中包含了该函数的5个拷贝。

#13


-   - 来个全面解析。

首先, inline 本身就C++程序来说没有语法意义: 无论一个函数是否inline,程序的执行结果是相同的。 inline的目的就是为了提高程序运行速度。


那么什么决定程序的运行速度呢? 我们不考虑文件I/O和系统调用,算法确定情况下,下面两样对程序执行速度有决定性的作用:

1 总指令执行量。 需要执行的总指令越多,就越需要CPU的执行开销。
  这个很容易定量。

2 流程中代码紧凑程度。 这关系到Cache换页频度。
  这个则是一个混沌的系数。 分析代码流才能确定影响力,我会慢慢解释。
  总的来说是这个规律:指令段越短小越好。


由于消除了调用执行和参数传递, inline 必然使流程总指令数变少;但是代码紧凑度则不然。

===================================================

和宏替代小函数道理一样,非常简短的 inline 替换,流程总指令数和紧凑度都会优化。
比如下面一个 get/set 方法:

class A{
  int _i;
public:
  int i() const{ return _i; }
};

这个 get 方法有两部分指令。 一部分是函数程序端, 包含函数的逻辑,这里是一个简单的根据 this 计算 _i 偏移,读入寄存器或者压栈 ret 返回。

另一部分则是调用端。
假如不使用 inline,那么下面这一行会引起函数调用:

n = a.i();
----------------------
lea ecx, a    // ecx 传递a的地址
call A::i      // 调用A::i, eax 为返回值
mov n, eax    // eax回写到n

我们来分析这一段的开销。调用端的时空开销一共为三行指令。 call 本身是一个稍大的指令,而调用的函数 A::i() 则不在当前代码段。 

就局部来说,这个调用涉及到其他代码段,降低了代码紧凑度。 假如当前cache中不包括这个函数代码段,他将引起cache换页。



假如使用inline,和刚才的call 等价的指令将放到调用端。
这个例子中无论怎么看都有好处:

n = a.i();
----------------------
mov eax, a._i // 一般是 ebp 直接计算
mov n, eax    // eax回写到n

代码仅仅有两条,而且少了call, 无需跳转,指令总数减少了,代码紧凑度也提高了。


===================================================

那么什么样的函数算大呢? 没有答案。

但是有一件事:只要函数 inline 后,调用端总代码超过函数调用代码,那么代码紧凑程度就有减弱的倾向。


现在我们不讨论大函数 inline展开,仅仅讨论非常小的函数。我们做一个假设:

假如某个函数 f ,调用端一个push, 一个call,一个回写参数和清栈,指令总长度为 20 字节。 

函数本身在很远的地方,他的代码段包括保护现场、栈操作、计算和恢复现场并返回,长度为80字节;而如果内联他,调用端只需要40字节就可以完成。( 看到拉?内联后大约10条指令,这实在是个小函数。 要知道, a +=  c 都要三条指令呢 )


那么就局部来看,非内联情况:

1 调用端 20 字节
2 远程函数体 80 字节

一共需要执行100字节的代码,远远不如内联的40字节。


但是我们再扩大一些范围看看?

假如有一个小函数 count, 他在一个循环中使用f、以及某些类似的小函数:

count( ... ){
  for( ... )
    if ( f1() == f2() ) f3() += f4();
    else if ( f1() == f3() ) f3() -= f4();
    else if ( !f1() ) f5();
}

上面这个简单的例子中,出现了10次 f 这样的小函数。 不内联的话,只需要200字节,加上其他指令大约300字节。 但是使用内联后,这个循环的“跨度” 就达到 500 字节了!

这本身也许不会引人注意;8086汇编中条件跳转只能跳跃 255 字节,这样增加到500字节,也许会迫使某些编译器把 for 中某些单一跳转拆为两条指令? 不过这都是小开销。


现在我们先不考虑巨范围程序开销。 考虑一下很多 count 这样不大的函数。 他们由于大小扩大一倍,所以相互的距离也扩大一倍。 本来小函数很有可能存放在相近的页面(比如某个类的成员函数)。 当连续使用那个类的成员函数时, 他们的总页面跨度也隐含着增大了。  本来一个20 K 的工作集就够了, 结果现在要30 K。 这已经开始影响到 cache 换页了。


越是往大范围看,这样的函数对紧密度的反作用越大。 考虑一个主循环,他调用某个工作集,包括很多高层函数,高层调用中层,中层调用下层....  这个工作集原本为 10M, 现在变成了20M....  于是完成同样的功能需要多走一倍的路。 


那么,就这个范围看看,内联 VS 非内联呢?
假如不内联,工作集变为10M, 外加一些小函数,共1K。 总指令增加2倍, 总工作集则减为一半。 最终是否提速? 很难说。 但是若是函数更大一点点呢?

===================================================

所以有一个说法叫:不要考虑优化。等真的需要优化,用测试软件测出瓶颈再优化也不迟。

为什么说,总有种提法,要优化最慢的5%?
因为,如果你优化全部的100%,性能可能会更低。



譬如一个函数f,他本来只有 80B ,占用95%的开销。

现在我把里面循环全部展开,配合查表法,让效率提高10倍,大小增加为 1K。

这个1K,对于程序松散度有不利影响;但是只有这里增加1K,对Cache影响不大。 程序提速了近10倍。



好嘛,假如某个家伙一鼓作气,所有代码一律展开循环查表等等... 程序变成原来10倍大... Cache狂换页... 你可以想象什么后果?

#14


“全部”函数都inline,意味着最终的结果是实际上只有一个函数。
于是再也没有函数的进入和退出,栈上的一个变量即使用完了,你也无法释放它占用的内存,哪怕你只用了一次。
如果Windows是这样做的,偶不知道1000G的内存够不够。。。。。

#15


当然,这只是偶直觉想到的一个直接后果,实际上这可能引出的问题估计多着呢。

#16


-  - 估计也差不多,而且那样Windows 就没有API和 DLL了。

#17


强人,学习

#18


都是些强人,,,学习。。。。。

#19


还有,递归调用怎么内联?如何事前能知道递归的层数?

#20


回楼上的
递归调用还好,记得数据结构上有写递归展开为循环的“机械”方法,可以代码化的。
不过一般C++ 编译器,一个函数只分配一次堆。 支持递归的话,得让函数动态申请堆内容(当然这也不难) 

... 发现这样YY下去挺有趣的?

#21


“让函数动态申请堆内容”,而“动态申请堆内容的函数”由于也是个函数,于是也需要“动态申请堆内容”,于是再调“动态申请堆内容的函数”,于是。。。。
结果为了递归,搞出了“递归的递归”,形式逻辑学科的一个新兴旁支就这么形成了。-_-

#22


嗯阿,不过说起来,复杂递归就麻烦的。  所以支持全部展开的编译器,必须对所有调用流分析,找到所有间接递归...   哈...  这个展开应该颇考验编译器作者技巧阿~~~

#23


切,当然不是函数了~~ 让编译器作者去做啊~~~

btw,说错了,动态申请栈内容。 ~~ 偶一直觉得C++ 几个主流编译器不厚道~~ 每个函数都一次把所有自动变量空间要着, 其实完全可以动态嘛~~

#24


如果长的函数也用内联,应用程序会变得笨重起来,因为他只是纯粹的代码拷贝!

#25


呵呵。

#26


mark

#27


内联函数中,不能含有复杂的结构控制语句,如SWITCH和WHILE。如果内联函数有这些语句,刚编译将该函数视同普通函数那样产生函数调用代码。
另外,递归函数是不能被用来做内联函数的。
——摘自《C++程序设计教程》钱能主编

#28


钱能?真武断阿。

下面的函数,VC7.1,Release模式。你看,我们不加 inline , VC都给我们inline了...

int func ( int n){
switch( n ){
case 1:
return 10;
case 2:
return 20;
default:
return 0;
}
}

int main(){
  cout<<func(2)<<endl;
}


看看 cout<<func(2)<<endl; 的输出:
-------------------------------------------
push 20 ; 00000014H
mov ecx, OFFSET FLAT:?cout@std@@3V?$basic_ostream@DU?$char_traits@D@std@@@1@A
call ??6?$basic_ostream@DU?$char_traits@D@std@@@std@@QAEAAV01@H@Z ; std::basic_ostream<char,std::char_traits<char> >::operator<<
....

直接就替换为20了... V

#29


递归函数中很多简单的尾递归形式可以被很多编译器优化为递推形式, 这样的递归函数照样可以被 inline , 不过做得很好的编译器不多,唉.
现在的有些编译器很智能鸟, 即使没加 inline , 最后简单的函数照样被 inline 处理, 这个好像 icl 做得比较好, 像:

#include <stdio.h>

int foobar( int i )
{
return 1000 + i;
}

int main()
{
__asm int 3;
printf( "%d\n" , foobar(10) );
return 0;


用 icl -O3 -fast test.c 编译后为:
0040100F 8B EC                mov         ebp,esp
00401011 83 EC 08             sub         esp,8
00401014 E8 2B 49 00 00       call        00405944
00401019 CC                   int         3
0040101A 68 F2 03 00 00       push        3F2h      ; foobar(10)=1010=0x3f2    bof
0040101F 68 B0 60 40 00       push        4060B0h   ; s "%d\n"
00401024 E8 0F 00 00 00       call        00401038  ; printf 
00401029 83 C4 08             add         esp,8
0040102C 33 C0                xor         eax,eax
0040102E 8B E5                mov         esp,ebp
00401030 5D                   pop         ebp
00401031 8B E3                mov         esp,ebx
00401033 5B                   pop         ebx
00401034 C3                   ret

#30


6.7.4
A function declared with an inline function specifier is an inline function. The function specifier may appear more than once; the behavior is the same as if it appeared only once. Making a function an inline function suggests that calls to the function be as fast as possible.118) The extent to which such suggestions are effective is implementation-defined.119)

118) By using, for example, an alternative to the usual function call mechanism, such as "inline substitution". Inline substitution is not textual substitution, nor does it create a new function.Therefore, for example, the expansion of a macro used within the body of the function uses the definition it had at the point the function body appears, and not where the function is called; and identifiers refer to the declarations in scope where the body occurs. Likewise, the function has a single address, regardless of the number of inline definitions that occur in addition to the external definition.
119) For example, an implementation might never perform inline substitution, or might only perform inline substitutions to calls in the scope of an inline declaration.

----------C99

7.1.2
2 A function declaration with an inline specifier declares an inline function. The inline specifier indicates to the implementation that inline substitution of the function body at the point of call is to be preferred to the usual function call mechanism. An implementation is not required to perform this inline substitution at the point of call; however, even if this inline substitution is omitted, the other rules for inline functions defined by 7.1.2 shall still be respected.

3 A function defined within a class definition is an inline function. The inline specifier shall not appear on a block scope function declaration.79)

4 An inline function shall be defined in every translation unit in which it is used and shall have exactly the same definition in every case. [Note: a call to the inline function may be encountered before its definition appears in the translation unit. ] If a function with external linkage is declared inline in one translation unit, it shall be declared inline in all translation units in which it appears; no diagnostic is required. An inline function with external linkage shall have the same address in all translation units. A static local variable in an extern inline function always refers to the same object. A string literal in an extern inline function is the same object in different translation units.

-----------C++03

#31


阿? 楼上不会是上次在群里讨论日文台词那个把....

#32


好像

#33


UP

#34


看看cpp的设计者是如何说的吧:
===================================摘自 cpp 设计与演化 
在带类的c的初始版本里没有提供inline函数,以从可用的表现形式中进一步得益。
当然,不久我们就提供了在线函数。引进在线函数的一般性理由与越过保护屏障的代价有关。
这种代价有可能导致人们不愿意去用类来隐藏表示的细节,特别的。[stroustrup,1982b]中
观察到人们总把数据成员做成公有的,以避免调用简单类的建构函数而带来的开销。
因为对这些对象的初始化可能只需要一两个赋值语句。将在线函数引进带类的c直接的原因
是一个具体的项目。在该项目中,由于某些类与实时处理有关,这种函数的调用开销无法接受。
为了使类机制能够成为在这个应用中有用的东西,就要求在跨越保护屏障时不付出任何代价。
只有在类声明中提供一种可用表示。并能把对公用(界面)函数的调用都变成在线的。
才能够达到这个目的。
==================================摘录结束

如作者所言,在认为支付调用函数的代价比较吃力时候才需要考虑使用内联函数。
(这一般都在实时系统里面)
后面还有一小节说在线机制的,不过太长了,我没有电子书,懒得抄了:)
也就是说,在需要时用inline的环境里,作者是很在乎效率的,
不会把一大堆的代码塞到函数中,不然,调用函数的开销和函数体的执行体开销相比就微乎其微了。

ox_thedarkness() 的分析挺细致的。
不过有些地方我有不同的看法。

----------------------------
1 总指令执行量。 需要执行的总指令越多,就越需要CPU的执行开销。
  这个很容易定量。

2 流程中代码紧凑程度。 这关系到Cache换页频度。
  这个则是一个混沌的系数。 分析代码流才能确定影响力, 我会慢慢解释。
  总的来说是这个规律:指令段越短小越好。
----------------------------
我想还应该加上些限制条件吧,
流水线cpu的效率很大程度上跟流水线被中断有关,
导致流水线中断主要是相关和条件转移,
相关指挨得比较近指令间有依赖关系。一条指令没有执行完,下一条也不能执行。
一条指令的执行分了好几个工序,这将导致可能会最多浪费一个指令周期(可能会很多个时钟周期),
但是问题是进入处理工序的指令也会等待这个指令周期。无形中让浪费加大了很多倍。
这种情况,一般都用乱序执行的方法:把后面的无关的指令提前到前面来,
条件转移指遇到带条件转移的指令需要在等执行完后才能决定那条指令被执行。
目前一般用分枝预测来解决,就是推测结果,预先装载预测的指令,当然猜测错了,代价也是很昂贵的。
因为流水线上的指令都要被废除。(据说intel的奔腾系列的cpu预测准确可达 80% , 不知道是真是假)。

因此cpu执行的开销不应该只是包括需要被执行的总指令,还包括那些被浪费的。


分析代码流并不能确定"影响力",呵呵。
==================摘自 量子混屯,这本书俺也看不懂 :(
混屯理论认为混屯运动之所以表现出随机特性,其根本原因在于邻近轨道
的指数型分离。换句话说。一个确定性的运动是否混屯,与它附近的
轨迹运动有密切关系。
。。。。
这种特征被称作运动对初值的敏感性,也称作指数型指数不稳定性。
描述这种特征需要用李雅普诺夫指数(lyapunov characteristic exponent)
。。。。。。
==================
在只考虑单道程序运行的例子中,
可以看作程序由代码流和数据流。
然后代码流的随机特性依赖于数据流。
经典的混屯理论认为在确定性的运动系统中。
人们能够根据初始状态推算系统的未来某一时刻的状态,
但是由于存在干扰因子,所以得到的推算值并不绝对正确。
这种干扰误差累积起来,经过很长一段时间后,结果将会和最初的预测大相径庭不可预测。
如果我们把代码流和数据流视作一个封闭的系统
(它是确定性的系统:给定代码流当前状态和数据流当前状态,我们可以确定代码流的下一个状态)。
那么代码流表现出来的随机特性取决于数据流。
比如代码中的条件转移部分需要根据数据流的状态来定。
相对于代码流,存在的条件转移和转移时的数据流,就是一个干扰因子。
这些干扰因子的效应累积起来后,将导致绝对正确的结果不可被预测。
例如,你在写完一段排序程序后,你能够绝对正确预测他的执行的效率么?
不可能,因为程序每次要处理的数据都不一样的。所以不可能在你写代码的时候就能够预测他准确的效率。

混屯理论还认为混屯是随机行为,即演化的过程回归到曾有的状态的附近。
然而,只是附近而已,也就是说每次都会有偏差。就是这些偏差,
会造成演化后的状态千姿百态。
程序是确定性的系统。在相对比较细的时间空间里,我们能够知道代码流执行的结果。
比如我们在debug程序的时候。我们总能精确的推测出有限的下几步是什么样子的,
但是如果再往下推测的时候,就会有越来越多的可能冒出来了。
每次我们面对不同数据的时候进行调试追踪的时候,可以看作是一个回归过程。
(如果强调每次数据都相同的情况下的调试,那么其中的干扰因子就不再是条件转移了,
而是别的了例如cpu执行的时序差磁盘io等等,在这么大的粒度下我们观测不到混屯)
所以,我们所谓的评估效率实际上是一些统计的平均值。不可能得到某个精确的值。
这也就是为什么很多算法例如搜索,排序会有最好情况,最坏情况的原因。


cache和workset理论的提出基于局部性原理。
----------------摘自 操作系统 开始
局部性原理(principle of locality):指程序在执行过程中的一个较短时期,所执行的指令地址和指令的操作数地址,
分别局限于一定区域。还可以表现为:
时间局部性,即一条指令的一次执行和下次执行,一个数据的一次访问和下次访问都集中在一个较短时期内;
空间局部性,即当前指令和邻近的几条指令,当前访问的数据和邻近的数据都集中在一个较小区域内。

局部性原理的具体体现
         程序在执行时,大部分是顺序执行的指令,少部分是转移和过程调用指令。
         过程调用的嵌套深度一般不超过5,因此执行的范围不超过这组嵌套的过程。
         程序中存在相当多的循环结构,它们由少量指令组成,而被多次执行。
         程序中存在相当多对一定数据结构的操作,如数组操作,往往局限在较小范围内。
----------------摘自 操作系统 结束

根据局部定理设置了cache,减少了内存访问的频率,从而有效的提高了系统效率。
独立出一层cache之后,又势必涉及到被独立出来的层与其他层的通讯。
比如何时进行通讯,进行什么样的通讯(读取,同步更新),通讯内容(通讯量等),才是最有效的。
于是workset理论应命而生。给局部性定理的描述进行量化的指导方向。
但是即使是workset也存在颠簸的现象。
这不是一个程序应该考虑的问题。个人觉得它更应该是操作系统要考虑的问题。

并不是函数体小就一定程序效率就高。还需要考虑数据,以及cache的读写策略,淘汰策略有关。
诚然,小的函数体从进入到退出,在cache没有到达临界值时,代码指令必然会有极高的命中率,
但是并不代表他们不需要更新到内存。比如这段代码是更新一段链表。
代码足够简单了,但是数据的内存分布却不一定在一起,所以cache还是会很快的超过临界值的。


此外函数体长也不一定导致程序低效。如果这段函数体都是有一些极小的循环构成,那么它们的效率也可能会很高的。
例子就不举了。只能说效率和函数体的大小并非线性相关。


有些离题了,本来mark下来,偷空写点回复的,没想到不小心思考上了,
呵呵,帖子已经结了,思考的内容食之五味,弃之可惜,权当借个地方放放吧。

#35


小声地说一声:如果真的需要用到了inline,那么推荐看一下
"组合语言设计之艺术" 作者:朱邦复 写作时间:1990年
虽然书有些老了,但是思想是没有老的,而且能够帮助你设计出高效的程序。

#36


在线函数?

#37


嗯,同意。我那贴主要是说紧凑程度是一个混沌的系数,在局部、大范围、宏观可能有不同的效果。

说的都是inline 的缺点。当然优点很多:

函数调用本身属于不确定因素,他强制隔断了上下流程。而 inline 的另一个作用是,让编译器有能力把上下文接起来继续优化。



不过,我建议不要写inline。

一来,几乎所有的程序大师都说过:不要考虑效率。需要考虑的时候,用剖析程序分析瓶颈再优化。 这不只是出于开发效率的考量。大部分优化技巧都是以空间为代价的,所以大面积优化在规模上的增长往往抵消了优化效果、甚至适得其反。 我举了一个例子。

二来,现在的编译器已经很聪明了。他们会进行开销/收益评估,而且会自动把非inline小函数 inline 化。上面已经有好几个例子说明这一点。

做C++ 优化,我个人认为应该相信编译器的优化, 警惕编译器的输出。 

前者因为C++编译器已经很好了。以前某人用C++写过一个复杂的memset,发现居然不如一个4行的版本。我这个汇编初学者试图用内嵌汇编比拼,在大数据时不相上下,小数据时仍然比不过。

后者是因为C++很多看起来很美的特性,底层可能产生你繁杂到匪夷所思的代码...

#1


因为不是每一个函数都能在编译期在调用处展开,如虚函数就不能.

#2


因为对于大函数来说,inline展开会令代码长度增加,带来的消耗反而更大。

#3


那什么样的函数才算了大函数呢?

#4


空间/时间的权衡问题吧

#5


不嫌打字麻烦,也不嫌编译器报一堆warning的情况下可以使用,但是记得一旦用了inline,那么函数实现必须与函数声明在同一个文件中,不能一个在.h,一个在.cpp

#6


那么函数实现必须与函数声明在同一个文件中,不能一个在.h,一个在.cpp
???

#7


inline是用于实现的,用于.cpp才有可能是内联的。声明的时候完全不需要的。
在林锐的《高质量C++编程指南》里面有详细的介绍

#8


既然inline可以免去函数的调用,那为什么不每个函数都用inline呢??
~~~~~~~~~~~~~~~
inline函数可以免去函数调用的损失,但是却大大增加了代码量(代码膨胀)
如果对所有函数使用inline,不可避免的是,一个编译文件将是是一个超大规模的
文件(所有函数都inline了),对于上大的工程文件,他的编译时间是不可忍受的!

inline对编译器只是一个提示说明,编译器可以对你的函数inline也可以不inline。
所以即使你的函数都写上inline,最后的结果,也可能一个inline也没有。如果有些函数确实
想采用inline,但是又怕编译给忽略掉,可以采用__inline,并在编译时刻选择
以__inline方式内联而不是默认方式内联就可。

#9


空间/时间的权衡问题吧
------------
同意

#10


那什么样的函数才算了大函数呢?
--------------------------------

一般函数体中含有循环,多个判断,或超过十几行的话,就算大函数,其调用开销是执行开销的高阶无穷小,可以忽略不计了.

#11


不行的
因为有些老的编译器如果把不能内联的函数内联会有问题的

#12


内联函数的执行速度比一般函数快,但是代价却是增加内存。
如果一个程序在5个地方调用同一个内联函数,相当于该程序中包含了该函数的5个拷贝。

#13


-   - 来个全面解析。

首先, inline 本身就C++程序来说没有语法意义: 无论一个函数是否inline,程序的执行结果是相同的。 inline的目的就是为了提高程序运行速度。


那么什么决定程序的运行速度呢? 我们不考虑文件I/O和系统调用,算法确定情况下,下面两样对程序执行速度有决定性的作用:

1 总指令执行量。 需要执行的总指令越多,就越需要CPU的执行开销。
  这个很容易定量。

2 流程中代码紧凑程度。 这关系到Cache换页频度。
  这个则是一个混沌的系数。 分析代码流才能确定影响力,我会慢慢解释。
  总的来说是这个规律:指令段越短小越好。


由于消除了调用执行和参数传递, inline 必然使流程总指令数变少;但是代码紧凑度则不然。

===================================================

和宏替代小函数道理一样,非常简短的 inline 替换,流程总指令数和紧凑度都会优化。
比如下面一个 get/set 方法:

class A{
  int _i;
public:
  int i() const{ return _i; }
};

这个 get 方法有两部分指令。 一部分是函数程序端, 包含函数的逻辑,这里是一个简单的根据 this 计算 _i 偏移,读入寄存器或者压栈 ret 返回。

另一部分则是调用端。
假如不使用 inline,那么下面这一行会引起函数调用:

n = a.i();
----------------------
lea ecx, a    // ecx 传递a的地址
call A::i      // 调用A::i, eax 为返回值
mov n, eax    // eax回写到n

我们来分析这一段的开销。调用端的时空开销一共为三行指令。 call 本身是一个稍大的指令,而调用的函数 A::i() 则不在当前代码段。 

就局部来说,这个调用涉及到其他代码段,降低了代码紧凑度。 假如当前cache中不包括这个函数代码段,他将引起cache换页。



假如使用inline,和刚才的call 等价的指令将放到调用端。
这个例子中无论怎么看都有好处:

n = a.i();
----------------------
mov eax, a._i // 一般是 ebp 直接计算
mov n, eax    // eax回写到n

代码仅仅有两条,而且少了call, 无需跳转,指令总数减少了,代码紧凑度也提高了。


===================================================

那么什么样的函数算大呢? 没有答案。

但是有一件事:只要函数 inline 后,调用端总代码超过函数调用代码,那么代码紧凑程度就有减弱的倾向。


现在我们不讨论大函数 inline展开,仅仅讨论非常小的函数。我们做一个假设:

假如某个函数 f ,调用端一个push, 一个call,一个回写参数和清栈,指令总长度为 20 字节。 

函数本身在很远的地方,他的代码段包括保护现场、栈操作、计算和恢复现场并返回,长度为80字节;而如果内联他,调用端只需要40字节就可以完成。( 看到拉?内联后大约10条指令,这实在是个小函数。 要知道, a +=  c 都要三条指令呢 )


那么就局部来看,非内联情况:

1 调用端 20 字节
2 远程函数体 80 字节

一共需要执行100字节的代码,远远不如内联的40字节。


但是我们再扩大一些范围看看?

假如有一个小函数 count, 他在一个循环中使用f、以及某些类似的小函数:

count( ... ){
  for( ... )
    if ( f1() == f2() ) f3() += f4();
    else if ( f1() == f3() ) f3() -= f4();
    else if ( !f1() ) f5();
}

上面这个简单的例子中,出现了10次 f 这样的小函数。 不内联的话,只需要200字节,加上其他指令大约300字节。 但是使用内联后,这个循环的“跨度” 就达到 500 字节了!

这本身也许不会引人注意;8086汇编中条件跳转只能跳跃 255 字节,这样增加到500字节,也许会迫使某些编译器把 for 中某些单一跳转拆为两条指令? 不过这都是小开销。


现在我们先不考虑巨范围程序开销。 考虑一下很多 count 这样不大的函数。 他们由于大小扩大一倍,所以相互的距离也扩大一倍。 本来小函数很有可能存放在相近的页面(比如某个类的成员函数)。 当连续使用那个类的成员函数时, 他们的总页面跨度也隐含着增大了。  本来一个20 K 的工作集就够了, 结果现在要30 K。 这已经开始影响到 cache 换页了。


越是往大范围看,这样的函数对紧密度的反作用越大。 考虑一个主循环,他调用某个工作集,包括很多高层函数,高层调用中层,中层调用下层....  这个工作集原本为 10M, 现在变成了20M....  于是完成同样的功能需要多走一倍的路。 


那么,就这个范围看看,内联 VS 非内联呢?
假如不内联,工作集变为10M, 外加一些小函数,共1K。 总指令增加2倍, 总工作集则减为一半。 最终是否提速? 很难说。 但是若是函数更大一点点呢?

===================================================

所以有一个说法叫:不要考虑优化。等真的需要优化,用测试软件测出瓶颈再优化也不迟。

为什么说,总有种提法,要优化最慢的5%?
因为,如果你优化全部的100%,性能可能会更低。



譬如一个函数f,他本来只有 80B ,占用95%的开销。

现在我把里面循环全部展开,配合查表法,让效率提高10倍,大小增加为 1K。

这个1K,对于程序松散度有不利影响;但是只有这里增加1K,对Cache影响不大。 程序提速了近10倍。



好嘛,假如某个家伙一鼓作气,所有代码一律展开循环查表等等... 程序变成原来10倍大... Cache狂换页... 你可以想象什么后果?

#14


“全部”函数都inline,意味着最终的结果是实际上只有一个函数。
于是再也没有函数的进入和退出,栈上的一个变量即使用完了,你也无法释放它占用的内存,哪怕你只用了一次。
如果Windows是这样做的,偶不知道1000G的内存够不够。。。。。

#15


当然,这只是偶直觉想到的一个直接后果,实际上这可能引出的问题估计多着呢。

#16


-  - 估计也差不多,而且那样Windows 就没有API和 DLL了。

#17


强人,学习

#18


都是些强人,,,学习。。。。。

#19


还有,递归调用怎么内联?如何事前能知道递归的层数?

#20


回楼上的
递归调用还好,记得数据结构上有写递归展开为循环的“机械”方法,可以代码化的。
不过一般C++ 编译器,一个函数只分配一次堆。 支持递归的话,得让函数动态申请堆内容(当然这也不难) 

... 发现这样YY下去挺有趣的?

#21


“让函数动态申请堆内容”,而“动态申请堆内容的函数”由于也是个函数,于是也需要“动态申请堆内容”,于是再调“动态申请堆内容的函数”,于是。。。。
结果为了递归,搞出了“递归的递归”,形式逻辑学科的一个新兴旁支就这么形成了。-_-

#22


嗯阿,不过说起来,复杂递归就麻烦的。  所以支持全部展开的编译器,必须对所有调用流分析,找到所有间接递归...   哈...  这个展开应该颇考验编译器作者技巧阿~~~

#23


切,当然不是函数了~~ 让编译器作者去做啊~~~

btw,说错了,动态申请栈内容。 ~~ 偶一直觉得C++ 几个主流编译器不厚道~~ 每个函数都一次把所有自动变量空间要着, 其实完全可以动态嘛~~

#24


如果长的函数也用内联,应用程序会变得笨重起来,因为他只是纯粹的代码拷贝!

#25


呵呵。

#26


mark

#27


内联函数中,不能含有复杂的结构控制语句,如SWITCH和WHILE。如果内联函数有这些语句,刚编译将该函数视同普通函数那样产生函数调用代码。
另外,递归函数是不能被用来做内联函数的。
——摘自《C++程序设计教程》钱能主编

#28


钱能?真武断阿。

下面的函数,VC7.1,Release模式。你看,我们不加 inline , VC都给我们inline了...

int func ( int n){
switch( n ){
case 1:
return 10;
case 2:
return 20;
default:
return 0;
}
}

int main(){
  cout<<func(2)<<endl;
}


看看 cout<<func(2)<<endl; 的输出:
-------------------------------------------
push 20 ; 00000014H
mov ecx, OFFSET FLAT:?cout@std@@3V?$basic_ostream@DU?$char_traits@D@std@@@1@A
call ??6?$basic_ostream@DU?$char_traits@D@std@@@std@@QAEAAV01@H@Z ; std::basic_ostream<char,std::char_traits<char> >::operator<<
....

直接就替换为20了... V

#29


递归函数中很多简单的尾递归形式可以被很多编译器优化为递推形式, 这样的递归函数照样可以被 inline , 不过做得很好的编译器不多,唉.
现在的有些编译器很智能鸟, 即使没加 inline , 最后简单的函数照样被 inline 处理, 这个好像 icl 做得比较好, 像:

#include <stdio.h>

int foobar( int i )
{
return 1000 + i;
}

int main()
{
__asm int 3;
printf( "%d\n" , foobar(10) );
return 0;


用 icl -O3 -fast test.c 编译后为:
0040100F 8B EC                mov         ebp,esp
00401011 83 EC 08             sub         esp,8
00401014 E8 2B 49 00 00       call        00405944
00401019 CC                   int         3
0040101A 68 F2 03 00 00       push        3F2h      ; foobar(10)=1010=0x3f2    bof
0040101F 68 B0 60 40 00       push        4060B0h   ; s "%d\n"
00401024 E8 0F 00 00 00       call        00401038  ; printf 
00401029 83 C4 08             add         esp,8
0040102C 33 C0                xor         eax,eax
0040102E 8B E5                mov         esp,ebp
00401030 5D                   pop         ebp
00401031 8B E3                mov         esp,ebx
00401033 5B                   pop         ebx
00401034 C3                   ret

#30


6.7.4
A function declared with an inline function specifier is an inline function. The function specifier may appear more than once; the behavior is the same as if it appeared only once. Making a function an inline function suggests that calls to the function be as fast as possible.118) The extent to which such suggestions are effective is implementation-defined.119)

118) By using, for example, an alternative to the usual function call mechanism, such as "inline substitution". Inline substitution is not textual substitution, nor does it create a new function.Therefore, for example, the expansion of a macro used within the body of the function uses the definition it had at the point the function body appears, and not where the function is called; and identifiers refer to the declarations in scope where the body occurs. Likewise, the function has a single address, regardless of the number of inline definitions that occur in addition to the external definition.
119) For example, an implementation might never perform inline substitution, or might only perform inline substitutions to calls in the scope of an inline declaration.

----------C99

7.1.2
2 A function declaration with an inline specifier declares an inline function. The inline specifier indicates to the implementation that inline substitution of the function body at the point of call is to be preferred to the usual function call mechanism. An implementation is not required to perform this inline substitution at the point of call; however, even if this inline substitution is omitted, the other rules for inline functions defined by 7.1.2 shall still be respected.

3 A function defined within a class definition is an inline function. The inline specifier shall not appear on a block scope function declaration.79)

4 An inline function shall be defined in every translation unit in which it is used and shall have exactly the same definition in every case. [Note: a call to the inline function may be encountered before its definition appears in the translation unit. ] If a function with external linkage is declared inline in one translation unit, it shall be declared inline in all translation units in which it appears; no diagnostic is required. An inline function with external linkage shall have the same address in all translation units. A static local variable in an extern inline function always refers to the same object. A string literal in an extern inline function is the same object in different translation units.

-----------C++03

#31


阿? 楼上不会是上次在群里讨论日文台词那个把....

#32


好像

#33


UP

#34


看看cpp的设计者是如何说的吧:
===================================摘自 cpp 设计与演化 
在带类的c的初始版本里没有提供inline函数,以从可用的表现形式中进一步得益。
当然,不久我们就提供了在线函数。引进在线函数的一般性理由与越过保护屏障的代价有关。
这种代价有可能导致人们不愿意去用类来隐藏表示的细节,特别的。[stroustrup,1982b]中
观察到人们总把数据成员做成公有的,以避免调用简单类的建构函数而带来的开销。
因为对这些对象的初始化可能只需要一两个赋值语句。将在线函数引进带类的c直接的原因
是一个具体的项目。在该项目中,由于某些类与实时处理有关,这种函数的调用开销无法接受。
为了使类机制能够成为在这个应用中有用的东西,就要求在跨越保护屏障时不付出任何代价。
只有在类声明中提供一种可用表示。并能把对公用(界面)函数的调用都变成在线的。
才能够达到这个目的。
==================================摘录结束

如作者所言,在认为支付调用函数的代价比较吃力时候才需要考虑使用内联函数。
(这一般都在实时系统里面)
后面还有一小节说在线机制的,不过太长了,我没有电子书,懒得抄了:)
也就是说,在需要时用inline的环境里,作者是很在乎效率的,
不会把一大堆的代码塞到函数中,不然,调用函数的开销和函数体的执行体开销相比就微乎其微了。

ox_thedarkness() 的分析挺细致的。
不过有些地方我有不同的看法。

----------------------------
1 总指令执行量。 需要执行的总指令越多,就越需要CPU的执行开销。
  这个很容易定量。

2 流程中代码紧凑程度。 这关系到Cache换页频度。
  这个则是一个混沌的系数。 分析代码流才能确定影响力, 我会慢慢解释。
  总的来说是这个规律:指令段越短小越好。
----------------------------
我想还应该加上些限制条件吧,
流水线cpu的效率很大程度上跟流水线被中断有关,
导致流水线中断主要是相关和条件转移,
相关指挨得比较近指令间有依赖关系。一条指令没有执行完,下一条也不能执行。
一条指令的执行分了好几个工序,这将导致可能会最多浪费一个指令周期(可能会很多个时钟周期),
但是问题是进入处理工序的指令也会等待这个指令周期。无形中让浪费加大了很多倍。
这种情况,一般都用乱序执行的方法:把后面的无关的指令提前到前面来,
条件转移指遇到带条件转移的指令需要在等执行完后才能决定那条指令被执行。
目前一般用分枝预测来解决,就是推测结果,预先装载预测的指令,当然猜测错了,代价也是很昂贵的。
因为流水线上的指令都要被废除。(据说intel的奔腾系列的cpu预测准确可达 80% , 不知道是真是假)。

因此cpu执行的开销不应该只是包括需要被执行的总指令,还包括那些被浪费的。


分析代码流并不能确定"影响力",呵呵。
==================摘自 量子混屯,这本书俺也看不懂 :(
混屯理论认为混屯运动之所以表现出随机特性,其根本原因在于邻近轨道
的指数型分离。换句话说。一个确定性的运动是否混屯,与它附近的
轨迹运动有密切关系。
。。。。
这种特征被称作运动对初值的敏感性,也称作指数型指数不稳定性。
描述这种特征需要用李雅普诺夫指数(lyapunov characteristic exponent)
。。。。。。
==================
在只考虑单道程序运行的例子中,
可以看作程序由代码流和数据流。
然后代码流的随机特性依赖于数据流。
经典的混屯理论认为在确定性的运动系统中。
人们能够根据初始状态推算系统的未来某一时刻的状态,
但是由于存在干扰因子,所以得到的推算值并不绝对正确。
这种干扰误差累积起来,经过很长一段时间后,结果将会和最初的预测大相径庭不可预测。
如果我们把代码流和数据流视作一个封闭的系统
(它是确定性的系统:给定代码流当前状态和数据流当前状态,我们可以确定代码流的下一个状态)。
那么代码流表现出来的随机特性取决于数据流。
比如代码中的条件转移部分需要根据数据流的状态来定。
相对于代码流,存在的条件转移和转移时的数据流,就是一个干扰因子。
这些干扰因子的效应累积起来后,将导致绝对正确的结果不可被预测。
例如,你在写完一段排序程序后,你能够绝对正确预测他的执行的效率么?
不可能,因为程序每次要处理的数据都不一样的。所以不可能在你写代码的时候就能够预测他准确的效率。

混屯理论还认为混屯是随机行为,即演化的过程回归到曾有的状态的附近。
然而,只是附近而已,也就是说每次都会有偏差。就是这些偏差,
会造成演化后的状态千姿百态。
程序是确定性的系统。在相对比较细的时间空间里,我们能够知道代码流执行的结果。
比如我们在debug程序的时候。我们总能精确的推测出有限的下几步是什么样子的,
但是如果再往下推测的时候,就会有越来越多的可能冒出来了。
每次我们面对不同数据的时候进行调试追踪的时候,可以看作是一个回归过程。
(如果强调每次数据都相同的情况下的调试,那么其中的干扰因子就不再是条件转移了,
而是别的了例如cpu执行的时序差磁盘io等等,在这么大的粒度下我们观测不到混屯)
所以,我们所谓的评估效率实际上是一些统计的平均值。不可能得到某个精确的值。
这也就是为什么很多算法例如搜索,排序会有最好情况,最坏情况的原因。


cache和workset理论的提出基于局部性原理。
----------------摘自 操作系统 开始
局部性原理(principle of locality):指程序在执行过程中的一个较短时期,所执行的指令地址和指令的操作数地址,
分别局限于一定区域。还可以表现为:
时间局部性,即一条指令的一次执行和下次执行,一个数据的一次访问和下次访问都集中在一个较短时期内;
空间局部性,即当前指令和邻近的几条指令,当前访问的数据和邻近的数据都集中在一个较小区域内。

局部性原理的具体体现
         程序在执行时,大部分是顺序执行的指令,少部分是转移和过程调用指令。
         过程调用的嵌套深度一般不超过5,因此执行的范围不超过这组嵌套的过程。
         程序中存在相当多的循环结构,它们由少量指令组成,而被多次执行。
         程序中存在相当多对一定数据结构的操作,如数组操作,往往局限在较小范围内。
----------------摘自 操作系统 结束

根据局部定理设置了cache,减少了内存访问的频率,从而有效的提高了系统效率。
独立出一层cache之后,又势必涉及到被独立出来的层与其他层的通讯。
比如何时进行通讯,进行什么样的通讯(读取,同步更新),通讯内容(通讯量等),才是最有效的。
于是workset理论应命而生。给局部性定理的描述进行量化的指导方向。
但是即使是workset也存在颠簸的现象。
这不是一个程序应该考虑的问题。个人觉得它更应该是操作系统要考虑的问题。

并不是函数体小就一定程序效率就高。还需要考虑数据,以及cache的读写策略,淘汰策略有关。
诚然,小的函数体从进入到退出,在cache没有到达临界值时,代码指令必然会有极高的命中率,
但是并不代表他们不需要更新到内存。比如这段代码是更新一段链表。
代码足够简单了,但是数据的内存分布却不一定在一起,所以cache还是会很快的超过临界值的。


此外函数体长也不一定导致程序低效。如果这段函数体都是有一些极小的循环构成,那么它们的效率也可能会很高的。
例子就不举了。只能说效率和函数体的大小并非线性相关。


有些离题了,本来mark下来,偷空写点回复的,没想到不小心思考上了,
呵呵,帖子已经结了,思考的内容食之五味,弃之可惜,权当借个地方放放吧。

#35


小声地说一声:如果真的需要用到了inline,那么推荐看一下
"组合语言设计之艺术" 作者:朱邦复 写作时间:1990年
虽然书有些老了,但是思想是没有老的,而且能够帮助你设计出高效的程序。

#36


在线函数?

#37


嗯,同意。我那贴主要是说紧凑程度是一个混沌的系数,在局部、大范围、宏观可能有不同的效果。

说的都是inline 的缺点。当然优点很多:

函数调用本身属于不确定因素,他强制隔断了上下流程。而 inline 的另一个作用是,让编译器有能力把上下文接起来继续优化。



不过,我建议不要写inline。

一来,几乎所有的程序大师都说过:不要考虑效率。需要考虑的时候,用剖析程序分析瓶颈再优化。 这不只是出于开发效率的考量。大部分优化技巧都是以空间为代价的,所以大面积优化在规模上的增长往往抵消了优化效果、甚至适得其反。 我举了一个例子。

二来,现在的编译器已经很聪明了。他们会进行开销/收益评估,而且会自动把非inline小函数 inline 化。上面已经有好几个例子说明这一点。

做C++ 优化,我个人认为应该相信编译器的优化, 警惕编译器的输出。 

前者因为C++编译器已经很好了。以前某人用C++写过一个复杂的memset,发现居然不如一个4行的版本。我这个汇编初学者试图用内嵌汇编比拼,在大数据时不相上下,小数据时仍然比不过。

后者是因为C++很多看起来很美的特性,底层可能产生你繁杂到匪夷所思的代码...