在C ++中调用函数有多少开销?

时间:2008-09-28 02:07:29

标签: c++ function overhead

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

16 个答案:

答案 0 :(得分:44)

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

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

答案 1 :(得分:11)

有技术和实际的答案。实际的答案是它永远不会重要,并且在非常罕见的情况下它会通过实际的分析测试来确定唯一的方法。

由于编译器优化,您的文献所引用的技术答案通常不相关。但如果您仍然感兴趣,Josh会很好地描述。

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

答案 2 :(得分:8)

开销量取决于编译器,CPU等。开销百分比取决于您内联的代码。要知道的唯一方法是使用您的代码并对其进行两种分析 - 这就是没有明确答案的原因。

答案 3 :(得分:8)

你的问题是其中一个问题,没有答案可以称之为绝对真理"。正常函数调用的开销取决于三个因素:

  1. CPU。 x86,PPC和ARM CPU的开销变化很大,即使您只使用一种架构,英特尔奔腾4,英特尔酷睿2双核和英特尔酷睿i7之间的开销也会有很大差异。即使两者都以相同的时钟速度运行,英特尔和AMD CPU之间的开销也可能明显不同,因为缓存大小,缓存算法,内存访问模式以及调用操作码本身的实际硬件实现等因素可能会产生巨大的影响。影响开销。

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

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

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

    但是,真正的问题是:确切的开销真的很重要吗?有一件事是肯定的:函数调用总是有开销。它可能很小,可能很大,但肯定存在。而且,如果在性能关键部分中经常调用一个函数有多小,那么开销在某种程度上也很重要。内联很少会使你的代码变慢,除非你非常过分;它会使代码更大。今天的编译器非常擅长决定自己什么时候内联,什么时候不进行内联,所以你几乎不用担心它的问题。

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

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

答案 4 :(得分:7)

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

<强> inc.c:

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

<强>的main.c

#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 上运行了十亿次迭代,这给了我:

    inlinining 版本 定期关联版本
  • 4.4秒

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

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

我测量的最快速度约为0.3ns,因此建议函数调用成本约为 9原始操作,非常简单。

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

答案 5 :(得分:5)

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

答案 6 :(得分:3)

值得指出的是,内联函数会增加调用函数的大小,任何增加函数大小的函数都可能对缓存产生负面影响。如果你正好在一个边界,“仅仅一个薄薄的薄荷”的内联代码可能会对性能产生巨大的负面影响。


如果您正在阅读有关“函数调用成本”警告的文献,我建议它可能是较旧的材料,不能反映现代处理器。除非你处于嵌入式世界,否则C是一种“便携式汇编语言”的时代已基本过去了。过去十年中芯片设计师的大量聪明才智(比如说)已经进入了各种各样的低级复杂性,这些复杂性与“白天”的工作方式大不相同。

答案 7 :(得分: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总是返回相同的结果,但好的优化器可以做到这一点。

答案 8 :(得分:1)

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

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

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

答案 9 :(得分: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

给出相同的结果,除了每种情况快几个数量级。

答案 10 :(得分:0)

每个新函数都需要创建一个新的本地堆栈。但是,只有在非常大量的迭代中对循环的每次迭代调用一个函数时,这才会引起注意。

答案 11 :(得分:0)

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

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

答案 12 :(得分:0)

我也没有任何数字,但我很高兴你问。我经常看到人们试图从模糊的开销想法开始优化他们的代码,但不是真的知道。

答案 13 :(得分:0)

这里有一些问题。

  • 如果你有一个足够聪明的编译器,它会为你做一些自动内联,即使你没有指定内联。另一方面,有许多事情无法内联。

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

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

答案 14 :(得分:0)

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

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

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

答案 15 :(得分:0)

正如其他人所说,你真的不必太担心开销,除非你想要获得最终的表现或类似的东西。当您创建函数时,编译器必须将代码写入:

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

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