我上课的培训材料似乎是两个相互矛盾的陈述。
一方面:
“使用内联函数通常会导致执行速度更快”
另一方面:
“使用内联函数可能会因频率降低而降低性能 交换“
问题1:两个陈述都是真的吗?
问题2:“交换”是什么意思?
请看一下这个片段:
int powA(int a, int b) {
return (a + b)*(a + b) ;
}
inline int powB(int a, int b) {
return (a + b)*(a + b) ;
}
int main () {
Timer *t = new Timer;
for(int a = 0; a < 9000; ++a) {
for(int b = 0; b < 9000; ++b) {
int i = (a + b)*(a + b); // 322 ms <-----
// int i = powA(a, b); // not inline : 450 ms
// int i = powB(a, b); // inline : 469 ms
}
}
double d = t->ms();
cout << "--> " << d << endl;
return 0;
}
问题3:为什么powA
和powB
之间的效果如此相似?我希望powB
的性能可以达到322毫秒,因为它毕竟是内联的。
答案 0 :(得分:5)
是的,在特定情况下,这两种陈述都可以是真实的。显然,他们不会同时同时。
“交换”可能是对OS分页行为的引用,当内存压力变高时,页面会被换出到磁盘。
实际上,如果您的内联函数很小,那么通常会因为消除函数调用和返回的开销而注意到性能提升。但是,在非常罕见的情况下,您可能会导致代码增长,使其无法完全驻留在CPU缓存中(在性能关键的紧密循环期间),并且您可能会遇到性能下降的问题。但是,如果您在该级别进行编码,那么您可能应该直接使用汇编语言进行编码。
inline
修饰符是编译器的提示,它可能要考虑编译内联的给定函数。它不必遵循您的指示,结果也可能取决于给定的编译器选项。您始终可以查看生成的汇编代码以了解它的作用。
您的基准测试可能甚至没有按照您的要求进行操作,因为您的编译器可能足够聪明,甚至没有使用您分配到i
的函数调用的结果,因此它甚至可能不会懒得打电话给你的功能。再看一下生成的汇编代码。
答案 1 :(得分:4)
inline
在调用站点插入代码,节省堆栈帧的创建,保存/恢复寄存器和调用(分支)。换句话说,使用inline
(当它工作时)类似于编写内联函数的代码来代替其调用。
但是,inline
不能保证做任何事情并且依赖于编译器。编译器有时候inline
函数不是内联的(好吧,它可能是链接器在链接时优化打开时执行的操作,但很容易想象可以在编译器级别完成的情况 - 例如当内联函数是静态的时候。)
如果要强制MSVC为inline
函数,请使用__forceinline
并检查程序集。应该没有调用 - 您的代码应该编译为线性执行的简单指令序列。
关于速度:通过内联小函数,您确实可以使代码更快。但是当你inline
大函数(和“大”很难定义,你需要运行测试来确定什么是大而什么不是)时,你的代码大小会变大。那是因为内联函数的代码在调用站点一遍又一遍地重复。毕竟,调用函数的重点是通过从代码中的多个位置重用相同的子例程来保存指令计数。
当代码大小变大时,指令缓存可能会不堪重负,导致代码执行速度变慢。
需要考虑的另一点:现代无序CPU(大多数桌面CPU - 例如Intel Core Duo或i7)都有一个机制(指令跟踪)来提前预取分支,然后在硬件级别预取“inline
” 。如此积极的内联并不总是有意义的。
在您的示例中,您需要查看编译器生成的程序集。 inline
和非inline
版本可能相同。如果它不是inline
,请尝试__forceinline
,如果它是您正在使用的MSVC。如果两种情况下的时间相同,则意味着您的CPU在预取指令方面做得很好,执行时间瓶颈在其他地方。
答案 2 :(得分:1)
交换是一个操作系统术语,用于将不同的内存页面交换进出正在运行的进程。基本上交换需要一些时间。你的应用程序越大,它的交换就越多。
当您内联函数时,不是跳转到单个子例程,而是将整个函数的副本转储到调用位置。这使您的程序更大,因此理论上可以导致更多的交换。
通常对于非常小的方法(比如你的powA和powB)内联应该没问题并且导致更快的执行,但它实际上只是“在理论上” - 在挤压最后的方面可能有“更大的鱼来炸”你的代码性能下降。
答案 3 :(得分:1)
书籍陈述是正确的。换句话说,正确完成后,inline
可以提高性能,如果做得不当可能会降低性能。
最好只内联小功能。这将减少额外的程序集调用以跳转内存。这就是性能提升的方式。
如果inline
大函数,则可能导致内存分页超出缓存大小,从而导致额外的内存交换。这就是性能受阻的原因。
答案 4 :(得分:1)
这两种说法都是正确的。声明函数inline
是编译器内联的指示符,如果能够的话。编译器将(通常)使用它自己的判断是否实际内联,但在C ++中声明它inline
确实改变了代码生成,至少对于符号生成。
此上下文中的“交换”是指将可执行映像分页到磁盘。由于可执行文件较大,因此可能会影响内存受限系统的性能。
回答第三个问题,编译器为两个函数选择了相同的行为(我的猜测是非内联的)。
答案 5 :(得分:1)
当编译普通函数时,它的机器代码被编译一次并放在一个与调用它的其他函数分开的位置。执行代码时,处理器必须跳转到存储代码的位置,并且此jump
指令需要额外的时间从内存加载函数。有时,需要几次跳转(或几次加载和跳转)来调用一个函数,例如虚函数。还有时间用于保存和恢复寄存器,以及创建堆栈帧,对于足够小的内联函数来说,这些都不是必需的。
编译内联函数时,其所有机器代码都直接插入到调用它的位置,因此消除了jump
指令的时间。编译器还根据其周围环境优化内联函数的代码(例如,寄存器赋值可以考虑在函数外部和函数内部使用的变量,以最小化需要保存的寄存器的数量)。但是,内联函数的代码可能出现在调用函数的多个位置(如果在调用代码中多次调用它),那么总的来说它会使代码库更大。这可能会导致代码增长到足以使其不再适合CPU缓存,在这种情况下,处理器必须转到主内存来获取代码,这比从缓存中获取所有内容要花费更长的时间。在某些情况下,这可以抵消消除jump
指令所带来的节省,并使代码比内联代码时慢。
“交换”通常是指虚拟内存的行为,它与CPU缓存具有相同的权衡,但是从磁盘加载代码所需的时间要长得多,而且程序所需的内存量也是如此。填补这个来发挥作用要大得多。您不太可能看到内联函数影响虚拟内存性能。
显然,这两种效果不会立即发生,但很难知道哪种效果适用于任何特定环境。