为什么编译器不内联所有内容?[关闭]


13

有时,编译器会内联函数调用。这意味着它们将被调用函数的代码移到调用函数中。这使事情变得更快一些,因为不需要在调用堆栈中上下移动内容。

所以我的问题是,为什么编译器不内联所有内容?我认为这将使可执行文件明显更快。

我能想到的唯一原因是可执行文件大得多,但是如今拥有数百GB内存真的重要吗?改进的性能值得吗?

还有其他原因导致编译器不仅仅内联所有函数调用吗?


18
IDK关于您,但我没有数百GB的内存。
Ampt

2
Isn't the improved performance worth it?对于将循环运行100次并处理一些严重数字的方法,将2或3个参数移至CPU寄存器的开销不大。
2014年

5
您过于笼统,“编译器”是指“所有编译器”,“所有”是否真的是“所有”?那么答案很简单,在某些情况下您根本无法内联。想到递归。
奥塔维奥·德西奥

17
缓存局部性比微小的函数调用开销重要得多。
SK-logic

3
如今,数百种GFLOPS的处理能力对性能提升的影响真的重要吗?
mouviciel 2014年

Answers:


22

首先请注意,内联的一个主要作用是它允许在呼叫站点进行进一步的优化。

对于您的问题:有些事情很难甚至不可能内联:

  • 动态链接库

  • 动态确定的函数(动态调度,通过函数指针调用)

  • 递归函数(可以进行尾递归)

  • 您没有代码的功能(但是链接时间优化允许其中的一些功能)

然后内联不仅具有有益的作用:

  • 更大的可执行文件意味着更多的磁盘空间和更长的加载时间

  • 较大的可执行文件意味着缓存压力增加(请注意,内联足够小的功能(例如简单的getter)可能会减小可执行文件的大小和缓存压力)

最后,对于花费不小的时间执行的函数而言,获得收益是不值得的。


3
可以内联一些递归调用(尾调用),但是如果您选择添加显式堆栈,则可以将所有递归调用转换为迭代
棘手怪胎2014年

@ratchetfreak,您还可以将一些非尾递归调用转换为尾尾。但这对我来说是“困难”的领域(尤其是当您具有共同递归函数或必须动态确定跳转到何处以模拟收益时),但这并不是不可能的(您只需放置一个延续框架并考虑到目前的情况变得更加容易)。
AProgrammer

11

一个主要限制是运行时多态性。如果在编写时发生动态调度foo.bar(),则无法内联方法调用。这解释了为什么编译器不内联所有内容。

递归调用也不能轻易地内联。

由于技术原因,跨模块内联也很难执行(例如,无法进行增量重新编译)

但是,编译器会内联很多事情。


3
通过虚拟调度进行内联非常困难,但并非不可能。一些C ++编译器在某些情况下可以做到这一点。
bstamour

2
...以及一些JIT编译器(非虚拟化)。
2014年

@bstamour任何具有适当优化的语言的半体面编译器都将静态分派(即,去虚拟化)对对象的已声明虚拟方法的调用,该对象的动态类型在编译时是已知的。如果去虚拟化阶段发生在(或另一个)内联阶段之前,这可以简化内联。但这是微不足道的。您还有其他意思吗?我看不到如何实现任何实际的“通过虚拟调度内联”。内联,一个人必须知道的静态类型-即devirtualise -所以内联方式的存在有没有虚拟调度
underscore_d

9

首先,您不能总是内联的,例如,递归函数可能并不总是线性的(但是可以内联包含递归定义的程序fact,仅包含的打印fact(8))。

然后,内联并不总是有益的。如果编译器内联太多,以至于结果代码足够大以至于其热部分无法放入例如L1指令缓存中,则它可能比非内联版本慢得多(后者很容易适合L1缓存)...同样,最近的处理器在执行CALL机器指令方面非常快(至少到已知位置,即直接调用,而不是通过指针的调用)。

最后,完整的内联需要对整个程序进行分析。这可能是不可能的(或成本太高)。使用GCC编译的C或C ++ (以及Clang / LLVM),您需要启用链接时间优化(通过例如eg进行编译和链接g++ -flto -O2),这需要大量的编译时间。


1
作为记录,LLVM / Clang(和其他几个编译器)还支持链接时优化

我知道; LTO存在于上个世纪(IIRC,至少在某些MIPS专有编译器中)。
Basile Starynkevitch 2014年

7

尽管看起来令人惊讶,但内联所有内容并不一定会减少执行时间。代码大小的增加会使CPU难以一次将所有代码保留在其缓存中。代码上的高速缓存未命中的可能性更高,并且高速缓存未命中的代价很高。如果您的潜在内联函数很大,则情况会更糟。

我不时地从头文件中取出大量标记为“内联”的代码,并将其放入源代码中,从而获得了明显的性能改进,因此代码仅位于一个位置,而不是每个调用站点。这样就可以更好地利用CPU缓存,并且还可以获得更好的编译时间。


这似乎只是重复一小时前发布的先前答案中提出和解释的观点
2014年

1
什么缓存?L1?L2?L3?哪一个更重要?
彼得·莫滕森

1

内联所有内容不仅意味着增加磁盘内存消耗,而且还意味着增加内部内存消耗,但这并不是很多。请记住,代码还依赖于代码段中的内存。如果从10000个位置调用一个函数(例如,在相当大的项目中是从标准库调用的),则该函数的代码将占用10000倍的内部内存。

另一个原因可能是JIT编译器。如果所有内容都是内联的,则没有热点可以动态编译。


1

第一,有简单的示例,其中内联所有内容的效果非常差。考虑以下简单的C代码:

void f1 (void) { printf ("Hello, world\n"); }
void f2 (void) { f1 (); f1 (); f1 (); f1 (); }
void f3 (void) { f2 (); f2 (); f2 (); f2 (); }
...
void f99 (void) { f98 (); f98 (); f98 (); f98 (); }

猜猜内联将对您产生什么影响。

接下来,您假设内联将使事情变得更快。有时候就是这种情况,但并非总是如此。原因之一是适合指令高速缓存的代码运行得快得多。如果我从10个地方调用一个函数,那么我将始终运行指令缓存中的代码。如果是内联的,则副本到处都是,并且运行慢得多。

还有其他问题:内联会产生巨大的功能。巨大的功能很难优化。通过将函数隐藏在单独的文件中以防止编译器对其内联,我在性能关键代码中获得了可观的收益。结果,隐藏这些函数时,为这些函数生成的代码要好得多。

顺便说一句。我没有“数百GB的内存”。我的工作计算机甚至没有“数百GB的硬盘空间”。而且,如果我的应用程序位于“数百GB的内存”中,则只需20分钟即可将应用程序加载到内存中。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.