关于C++内联函数的几个问题

Few questions about C++ inline functions

本文关键字:几个问题 函数 C++ 关于      更新时间:2023-10-16

我上课的培训材料似乎做了两个相互矛盾的陈述。

一方面:

"使用内联函数通常会导致更快的执行速度"

另一方面:

"使用内联函数可能会降低性能,因为更频繁 交换"

问题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:为什么powApowB之间的性能如此相似?我本来预计powB性能会达到 322 毫秒,因为它毕竟是内联的。

问题 1

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

问题2

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

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

问题3

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

你的基准测试甚至可能没有做你想要的,因为你的编译器可能足够聪明,可以看到你甚至没有使用你分配给i的函数调用的结果,所以它甚至可能懒得调用你的函数。再次查看生成的程序集代码。

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

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

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

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

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

需要考虑的另一点:现代无序CPU(大多数台式机CPU - 例如英特尔酷睿双核或i7)具有一种机制(指令跟踪),可以在硬件级别预先取分支并"inline"。因此,积极的内联并不总是有意义的。

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

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

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

通常对于非常小的方法(如powA和powB),内联应该没问题,并且可以更快地执行,但这实际上只是"理论上" - 在从代码中挤出最后一滴性能方面,可能有"更大的鱼要炸"。

书籍陈述是正确的。 换句话说,如果操作得当,inline可以提高性能,而如果操作不当,可能会降低性能。

最好只内联小函数。 这将减少跳转内存的额外程序集调用。 这就是提高性能的方式。

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

这两种说法都是正确的,有点。 声明函数inline是编译器内联的指示器(如果可能)。 编译器(通常)会使用自己的判断来判断是否实际内联,但在C++声明它时inline确实会更改代码生成,至少对于符号生成而言。

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

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

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

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

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

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