4

我参加的课程中的培训材料似乎做出了两个相互矛盾的陈述。

一方面:

“使用内联函数通常会加快执行速度”

另一方面:

“由于更频繁的交换,使用内联函数可能会降低性能”

问题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 毫秒,因为它毕竟是内联的。

4

6 回答 6

5

问题 1

是的,在特定情况下,这两种说法都可能是真的。显然它们不会同时为

问题2

“交换”可能是对操作系统分页行为的引用,当内存压力变高时,页面被换出到磁盘。

实际上,如果您的内联函数很小,那么由于消除了函数调用和返回的开销,您通常会注意到性能改进。但是,在极少数情况下,您可能会导致代码增长到无法完全驻留在 CPU 缓存中(在性能关键的紧密循环期间),并且您可能会遇到性能下降的情况。但是,如果您在该级别进行编码,那么您可能应该直接用汇编语言进行编码。

问题 3

inline修饰符是对编译器的提示它可能要考虑内联编译给定的函数。它不必遵循您的指示,结果也可能取决于给定的编译器选项。您始终可以查看生成的汇编代码以了解它的作用。

你的基准测试甚至可能没有做你想做的事情,因为你的编译器可能足够聪明,可以看到你甚至没有使用你分配给的函数调用的结果i,所以它甚至可能不会费心去调用你的函数。再次查看生成的汇编代码。

于 2012-08-13T01:54:52.010 回答
4

inline在调用站点插入代码,保存堆栈帧的创建,保存/恢复寄存器和调用(分支)。换句话说,使用inline(当它工作时)类似于为内联函数编写代码来代替它的调用。

但是,inline不能保证做任何事情并且依赖于编译器。编译器有时会inline使用非内联函数(好吧,当链接时优化打开时,它可能是链接器执行此操作,但很容易想象可以在编译器级别完成的情况 - 例如,当内联函数是静止的)。

如果要强制 MSVCinline运行,请使用__forceinline并检查程序集。不应该有调用——你的代码应该编译成线性执行的简单指令序列。

关于速度:您确实可以通过内联小函数使您的代码更快。但是,当您使用inline大型函数时(并且“大型”很难定义,您需要运行测试以确定什么是大的,什么不是),您的代码大小会变得更大。这是因为内联函数的代码在调用站点一遍又一遍地重复。毕竟,调用函数的全部意义在于通过在代码中的多个位置重用同一个子例程来节省指令数。

当代码变大时,指令缓存可能会不堪重负,导致代码执行速度变慢。

需要考虑的另一点:现代无序 CPU(大多数台式机 CPU - 例如 Intel Core Duo 或 i7)有一种机制(指令跟踪)可以提前预取分支,inline然后在硬件级别“”。所以激进的内联并不总是有意义的。

在您的示例中,您需要查看编译器生成的程序集。inline版本和非inline版本可能相同。如果不是inline,请尝试__forceinline您正在使用的是否是 MSVC。如果两种情况下的时序相同,则意味着您的 CPU 在预取指令方面做得很好,而执行时间瓶颈在其他地方。

于 2012-08-13T02:02:04.253 回答
1

交换是一个操作系统术语,用于将不同的内存页面换入和换出正在运行的进程。基本上交换需要一些时间。你的应用程序越大,它可能有越多的交换。

当您内联一个函数时,不是跳转到单个子例程,而是将整个函数的副本转储到调用位置。这会使你的程序更大,因此理论上可以导致更多的交换。

通常对于非常小的方法(例如您的 powA 和 powB),内联应该没问题并导致更快的执行,但这实际上只是“理论上” - 就挤压最后一滴性能而言,可能有“更大的鱼要炸”出你的代码。

于 2012-08-13T01:56:18.503 回答
1

书上的陈述是正确的。换句话说,如果做得好,inline可以提高性能,如果做得不好,会降低性能。

最好只内联小函数。这将减少额外的汇编调用以跳转到内存中。这就是提高性能的方式。

如果您使用inline大型函数,这可能会导致内存分页超出缓存大小,从而导致额外的内存交换。这就是性能受到阻碍的方式。

于 2012-08-13T01:56:20.743 回答
1

两种说法都是真的,有点。声明一个函数inline是编译器在可能的情况下进行内联的一个指标。编译器将(通常)使用自己的判断来判断是否实际内联,但在 C++ 中声明它inline确实会改变代码生成,至少对于符号生成而言。

在此上下文中,“交换”是指将可执行映像分页到磁盘。由于可执行文件较大,它可能会影响内存受限系统的性能。

在回答您的第三个问题时,编译器为这两个函数选择了相同的行为(我的猜测是非内联的)。

于 2012-08-13T01:57:50.267 回答
1

当一个普通函数被编译时,它的机器代码被编译一次,并与调用它的其他函数分开放置在一个地方。执行代码时,处理器必须跳转到存储代码的地方,这jump条指令需要额外的时间从内存中加载函数。有时,调用一个函数(例如虚函数)需要多次跳转(或多次加载和一次跳转)。还有一些时间用于保存和恢复寄存器,以及创建堆栈帧,对于足够小的内联函数来说,这些都不是真正必要的。

当一个内联函数被编译时,它的所有机器码都直接插入到它被调用的地方,所以jump指令被淘汰。编译器还会根据其环境优化内联函数的代码(例如寄存器赋值可以同时考虑函数外部和函数内部使用的变量,以尽量减少需要保存的寄存器数量)。但是,内联函数的代码可能会出现在调用函数的多个位置(如果它在调用代码中被多次调用),因此总的来说它会使您的代码库更大。这可能会导致您的代码变得足够大,以至于它不再适合 CPU 缓存,在这种情况下,处理器必须进入主内存来获取您的代码,这比从缓存中获取所有内容需要更长的时间。在某些情况下,这可以抵消消除jump指令,并使您的代码比内联代码慢。

“交换”通常是指虚拟内存的行为,它与 CPU 缓存具有相同的权衡,但是从磁盘加载代码所花费的时间要长得多,并且您的程序必须为此填充的内存量发挥更大的作用。您不太可能看到内联函数会影响虚拟内存性能。

显然这两种影响不会同时发生,但很难知道在任何给定情况下哪种会适用。

于 2012-08-13T01:59:56.600 回答