67

许多文献都谈到使用内联函数来“避免函数调用的开销”。但是我还没有看到可量化的数据。函数调用的实际开销是多少,即我们通过内联函数实现了什么样的性能提升?

4

16 回答 16

48

在大多数体系结构中,成本包括将所有(或一些,或没有)寄存器保存到堆栈,将函数参数推送到堆栈(或将它们放入寄存器),递增堆栈指针并跳转到开头新代码。然后当函数完成后,你必须从堆栈中恢复寄存器。 该网页描述了各种调用约定所涉及的内容。

大多数 C++ 编译器现在都足够聪明,可以为您内联函数。inline 关键字只是对编译器的提示。有些人甚至会在他们认为有帮助的翻译单元中进行内联。

于 2008-09-28T02:14:31.123 回答
24

我对一个简单的增量函数做了一个简单的基准测试:

公司:

typedef unsigned long ulong;
ulong inc(ulong x){
    return x+1;
}

主程序

#include <stdio.h>
#include <stdlib.h>

typedef unsigned long ulong;

#ifdef EXTERN 
ulong inc(ulong);
#else
static inline ulong inc(ulong x){
    return x+1;
}
#endif

int main(int argc, char** argv){
    if (argc < 1+1)
        return 1;
    ulong i, sum = 0, cnt;
    cnt = atoi(argv[1]);
    for(i=0;i<cnt;i++){
        sum+=inc(i);
    }
    printf("%lu\n", sum);
    return 0;
}

在我的Intel(R) Core(TM) i5 CPU M 430 @ 2.27GHz上运行十亿次迭代给了我:

  • 内联版本为1.4 秒
  • 定期链接版本为4.4 秒

(它似乎波动高达 0.2,但我懒得计算正确的标准偏差,也不关心它们)

这表明这台计算机上的函数调用开销约为3 纳秒

我测量的最快的东西大约是 0.3ns,所以这表明一个函数调用大约需要9 个原始操作,简单地说。

对于通过 PLT 调用的函数(共享库中的函数),每次调用的开销会增加大约2ns(总调用时间约为6ns)。

于 2016-08-07T14:31:26.977 回答
12

有技术和实际的答案。实际的答案是它永远不会重要,而且在极少数情况下,你知道的唯一方法是通过实际的分析测试。

由于编译器优化,您的文献引用的技术答案通常不相关。但是,如果您仍然感兴趣,那么Josh对此进行了很好的描述。

就“百分比”而言,您必须知道该功能本身的成本。在被调用函数的成本之外,没有百分比,因为您正在与零成本操作进行比较。对于内联代码没有成本,处理器只是移动到下一条指令。inling 的缺点是代码量较大,这表明它的成本与堆栈构建/拆卸成本不同。

于 2008-09-28T02:53:14.180 回答
9

您的问题是问题之一,没有答案可以称为“绝对真理”。普通函数调用的开销取决于三个因素:

  1. 中央处理器。x86、PPC 和 ARM CPU 的开销变化很大,即使您只使用一种架构,Intel Pentium 4、Intel Core 2 Duo 和 Intel Core i7 之间的开销也有很大差异。即使英特尔和 AMD CPU 以相同的时钟速度运行,开销甚至可能会显着不同,因为缓存大小、缓存算法、内存访问模式和调用操作码本身的实际硬件实现等因素可能会产生巨大的影响。对开销的影响。

  2. ABI(应用程序二进制接口)。即使使用相同的 CPU,也经常存在不同的 ABI,它们指定函数调用如何传递参数(通过寄存器、堆栈或两者的组合)以及堆栈帧初始化和清理的位置和方式。所有这些都会对开销产生影响。不同的操作系统可能对同一个 CPU 使用不同的 ABI;例如,Linux、Windows 和 Solaris 可能都对同一个 CPU 使用不同的 ABI。

  3. 编译器。只有在独立代码单元之间调用函数时,严格遵循 ABI 才重要,例如,如果应用程序调用系统库的函数或用户库调用另一个用户库的函数。只要函数是“私有的”,在某个库或二进制文件之外不可见,编译器就可能“作弊”。它可能不严格遵循 ABI,而是使用导致更快函数调用的快捷方式。例如,它可以在寄存器中传递参数而不是使用堆栈,或者如果不是真的需要,它可以完全跳过堆栈帧设置和清理。

如果您想了解上述三个因素的特定组合的开销,例如对于使用 GCC 的 Linux 上的 Intel Core i5,您获取此信息的唯一方法是对两种实现之间的差异进行基准测试,一种使用函数调用,另一种使用您将代码直接复制到调用者中;这样你肯定会强制内联,因为内联语句只是一个提示,并不总是导致内联。

然而,这里真正的问题是:确切的开销真的很重要吗?有一件事是肯定的:函数调用总是有开销。它可能很小,可能很大,但它肯定存在。而且,如果一个函数在性能关键部分被足够频繁地调用,那么无论它多么小,开销都会在某种程度上很重要。内联很少会使您的代码变慢,除非您过度使用它;它会使代码更大。今天的编译器非常擅长决定何时内联和何时不内联,因此您几乎不必为此绞尽脑汁。

就我个人而言,我在开发过程中完全忽略了内联,直到我有一个或多或少可用的产品,我可以分析并且只有当分析告诉我,某个函数确实经常被调用并且在应用程序的性能关键部分内,然后我会考虑这个函数的“强制内联”。

到目前为止,我的回答非常通用,它适用于 C 就像它适用于 C++ 和 Objective-C 一样。作为结束语,让我特别谈谈 C++:虚拟方法是双重间接函数调用,这意味着它们比普通函数调用具有更高的函数调用开销,而且它们不能被内联。非虚拟方法可能被编译器内联或不内联,但即使它们没有内联,它们仍然比虚拟方法快得多,所以你不应该将方法设为虚拟,除非你真的打算覆盖它们或让它们被覆盖。

于 2012-03-03T20:31:34.420 回答
8

开销的数量取决于编译器、CPU 等。百分比开销将取决于您内联的代码。知道的唯一方法是获取您的代码并以两种方式对其进行分析 - 这就是为什么没有明确的答案。

于 2008-09-28T02:30:21.693 回答
5

对于非常小的函数,内联是有意义的,因为函数调用的(小)成本相对于函数体的(非常小的)成本是显着的。对于几行上的大多数功能来说,这并不是一个很大的胜利。

于 2008-09-28T02:10:05.423 回答
4

It's worth pointing out that an inlined function increases the size of the calling function and anything that increases the size of a function may have a negative affect on caching. If you're right at a boundary, "just one more wafer thin mint" of inlined code might have a dramatically negative effect on performance.


If you're reading literature that's warning about "the cost of a function call," I'd suggest it may be older material that doesn't reflect modern processors. Unless you're in the embedded world, the era in which C is a "portable assembly language" has essentially passed. A large amount of the ingenuity of the chip designers in the past decade (say) has gone into all sorts of low-level complexities that can differ radically from the way things worked "back in the day."

于 2008-10-30T17:44:59.627 回答
2

有一个很棒的概念称为“寄存器阴影”,它允许通过寄存器(在 CPU 上)而不是堆栈(内存)传递(最多 6 个?)值。此外,根据其中使用的函数和变量,编译器可能只是决定不需要帧管理代码!

此外,即使是 C++ 编译器也可能会进行“尾递归优化”,即如果 A() 调用 B(),并且在调用 B() 之后,A 刚刚返回,编译器将重用堆栈帧!

当然,这一切都可以做到,只要程序坚持标准的语义(参见指针别名及其对优化的影响)

于 2008-09-28T02:43:14.547 回答
2

现代 CPU 非常快(显然!)。几乎所有涉及调用和参数传递的操作都是全速指令(间接调用可能稍微贵一些,主要是第一次通过循环)。

函数调用开销是如此之小,只有调用函数的循环才能使调用开销相关。

因此,当我们今天谈论(和衡量)函数调用开销时,我们通常实际上是在谈论无法将公共子表达式提升到循环之外的开销。如果一个函数每次被调用时都必须做一堆(相同的)工作,那么编译器将能够将它“提升”出循环并在它被内联时执行一次。当没有内联时,代码可能会继续重复工作,你告诉它!

内联函数似乎不可能更快不是因为调用和参数开销,而是因为可以从函数中提升的公共子表达式。

例子:

Foo::result_type MakeMeFaster()
{
  Foo t = 0;
  for (auto i = 0; i < 1000; ++i)
    t += CheckOverhead(SomethingUnpredictible());
  return t.result();
}

Foo CheckOverhead(int i)
{
  auto n = CalculatePi_1000_digits();
  return i * n;
}

优化器可以看穿这种愚蠢并执行以下操作:

Foo::result_type MakeMeFaster()
{
  Foo t;
  auto _hidden_optimizer_tmp = CalculatePi_1000_digits();
  for (auto i = 0; i < 1000; ++i)
    t += SomethingUnpredictible() * _hidden_optimizer_tmp;
  return t.result();
}

似乎调用开销不可能减少,因为它确实已经从循环中提升了一大块函数(CalculatePi_1000_digits 调用)。编译器需要能够证明CalculatePi_1000_digits 总是返回相同的结果,但是好的优化器可以做到这一点。

于 2013-01-15T00:15:34.240 回答
1

这里有几个问题。

  • 如果你有一个足够聪明的编译器,即使你没有指定内联,它也会为你做一些自动内联。另一方面,有很多东西不能内联。

  • 如果函数是虚拟的,那么您当然要付出无法内联的代价,因为目标是在运行时确定的。相反,在 Java 中,您可能会付出这个代价,除非您指出该方法是最终的。

  • 根据您的代码在内存中的组织方式,您可能会为缓存未命中甚至页面未命中付出代价,因为代码位于其他位置。这最终可能会对某些应用程序产生巨大影响。

于 2008-09-28T02:39:45.383 回答
1

根本没有太多开销,尤其是对于小型(可内联)函数甚至类。

以下示例具有三个不同的测试,每个测试都运行很多次并计时。结果总是等于单位时间的千分之一。

#include <boost/timer/timer.hpp>
#include <iostream>
#include <cmath>

double sum;
double a = 42, b = 53;

//#define ITERATIONS 1000000 // 1 million - for testing
//#define ITERATIONS 10000000000 // 10 billion ~ 10s per run
//#define WORK_UNIT sum += a + b
/* output
8.609619s wall, 8.611255s user + 0.000000s system = 8.611255s CPU(100.0%)
8.604478s wall, 8.611255s user + 0.000000s system = 8.611255s CPU(100.1%)
8.610679s wall, 8.595655s user + 0.000000s system = 8.595655s CPU(99.8%)
9.5e+011 9.5e+011 9.5e+011
*/

#define ITERATIONS 100000000 // 100 million ~ 10s per run
#define WORK_UNIT sum += std::sqrt(a*a + b*b + sum) + std::sin(sum) + std::cos(sum)
/* output
8.485689s wall, 8.486454s user + 0.000000s system = 8.486454s CPU (100.0%)
8.494153s wall, 8.486454s user + 0.000000s system = 8.486454s CPU (99.9%)
8.467291s wall, 8.470854s user + 0.000000s system = 8.470854s CPU (100.0%)
2.50001e+015 2.50001e+015 2.50001e+015
*/


// ------------------------------
double simple()
{
   sum = 0;
   boost::timer::auto_cpu_timer t;
   for (unsigned long long i = 0; i < ITERATIONS; i++)
   {
      WORK_UNIT;
   }
   return sum;
}

// ------------------------------
void call6()
{
   WORK_UNIT;
}
void call5(){ call6(); }
void call4(){ call5(); }
void call3(){ call4(); }
void call2(){ call3(); }
void call1(){ call2(); }

double calls()
{
   sum = 0;
   boost::timer::auto_cpu_timer t;

   for (unsigned long long i = 0; i < ITERATIONS; i++)
   {
      call1();
   }
   return sum;
}

// ------------------------------
class Obj3{
public:
   void runIt(){
      WORK_UNIT;
   }
};

class Obj2{
public:
   Obj2(){it = new Obj3();}
   ~Obj2(){delete it;}
   void runIt(){it->runIt();}
   Obj3* it;
};

class Obj1{
public:
   void runIt(){it.runIt();}
   Obj2 it;
};

double objects()
{
   sum = 0;
   Obj1 obj;

   boost::timer::auto_cpu_timer t;
   for (unsigned long long i = 0; i < ITERATIONS; i++)
   {
      obj.runIt();
   }
   return sum;
}
// ------------------------------


int main(int argc, char** argv)
{
   double ssum = 0;
   double csum = 0;
   double osum = 0;

   ssum = simple();
   csum = calls();
   osum = objects();

   std::cout << ssum << " " << csum << " " << osum << std::endl;
}

运行 10,000,000 次迭代(每种类型:简单、六个函数调用、三个对象调用)的输出是使用这个半复杂的工作负载:

sum += std::sqrt(a*a + b*b + sum) + std::sin(sum) + std::cos(sum)

如下:

8.485689s wall, 8.486454s user + 0.000000s system = 8.486454s CPU (100.0%)
8.494153s wall, 8.486454s user + 0.000000s system = 8.486454s CPU (99.9%)
8.467291s wall, 8.470854s user + 0.000000s system = 8.470854s CPU (100.0%)
2.50001e+015 2.50001e+015 2.50001e+015

使用一个简单的工作负载

sum += a + b

给出相同的结果,但每种情况下的速度要快几个数量级。

于 2015-02-09T20:59:16.543 回答
1

正如其他人所说,您真的不必过多担心开销,除非您要追求终极性能或类似的东西。当您创建一个函数时,编译器必须编写代码:

  • 将函数参数保存到堆栈
  • 将返回地址保存到堆栈中
  • 跳转到函数的起始地址
  • 为函数的局部变量分配空间(堆栈)
  • 运行函数体
  • 保存返回值(堆栈)
  • 局部变量(即垃圾收集)的可用空间
  • 跳转回保存的退货地址
  • 释放保存参数等...

但是,您必须考虑降低代码的可读性,以及它将如何影响您的测试策略、维护计划和 src 文件的整体大小影响。

于 2017-09-23T05:29:20.013 回答
0

每个新函数都需要创建一个新的本地堆栈。但是,只有当您在循环的每次迭代中调用一个函数并进行大量迭代时,这种开销才会很明显。

于 2008-09-28T02:14:51.293 回答
0

对于大多数函数,在 C++ 和 C 中调用它们并没有额外的开销(除非您将“this”指针视为每个函数的不必要参数。您必须以某种方式将状态传递给函数)...

对于虚函数,它们是一个额外的间接级别(相当于通过 C 中的指针调用函数)......但实际上,在今天的硬件上,这是微不足道的。

于 2008-09-28T02:14:56.240 回答
0

我也没有任何数字,但我很高兴你问。我经常看到人们尝试从开销的模糊概念开始优化他们的代码,但并不真正了解。

于 2008-09-28T02:38:24.170 回答
0

根据您构建代码的方式、划分为模块和库等单元的方式,在某些情况下它可能非常重要。

  1. 使用带有外部链接的动态库函数大多数时候会强制执行完整的堆栈帧处理。
    这就是为什么当比较操作像整数比较一样简单时,使用 stdc 库中的 qsort 比使用 stl 代码慢一个数量级(10 倍)。
  2. 在模块之间传递函数指针也会受到影响。
  3. 同样的惩罚很可能会影响 C++ 的虚函数以及其他函数的使用,这些函数的代码是在单独的模块中定义的。

  4. 好消息是整个程序优化可能会解决静态库和模块之间的依赖关系问题。

于 2016-02-09T11:09:50.283 回答