多少开销那里调用C ++函数?(How much overhead is there in call

2019-07-18 15:51发布

很多关于使用内联函数“避免函数调用的开销”文学会谈。 但是我还没有看到量化的数据。 什么是一个函数调用,即我们通过内联函数达到什么样的性能提升的实际开销?

Answer 1:

在大多数架构中,成本包括保存所有(或部分或无)寄存器堆栈,推函数参数堆栈(或将他们寄存器),递增堆栈指针和跳跃到的初新代码。 然后,当函数完成后,你必须从堆栈恢复寄存器。 这个网页有什么参与各种调用约定的说明。

大多数C ++编译器有足够的智慧现在内联函数你。 inline关键字仅仅是一个暗示,编译器。 有些人甚至会做跨翻译单元,他们决定它是有帮助的内联。



Answer 2:

还有的技术和实际的答案。 实际的答案是永远不会无所谓了,在非常罕见的情况下,它不会你知道的唯一途径是通过实际测试异形。

该技术的答案,你的文学是指,一般是不相关的,由于编译器优化。 但是,如果你想继续,深受描述乔希 。

至于是“百分比”你必须知道该函数本身有多贵了。 被调用函数的成本之外,没有百分比,因为你比作零成本操作。 对于内联的代码是没有成本的,处理器只是移动到下一个指令。 该缺点inling是一个较大的代码大小,其表现是在比堆栈结构以不同的方式费用/拆除费用。



Answer 3:

开销的数量将取决于编译器,CPU,等百分比开销将取决于你的内联代码。 要知道唯一的办法就是把你的代码和配置文件两者兼得-这就是为什么没有明确的答案。



Answer 4:

你的问题的问题,是一直无人接听一个可称之为“绝对真理”之一。 一个正常的函数调用的开销取决于三个因素:

  1. 该CPU。 86,PPC和ARM CPU的开销差异很大,即使你只是呆在一个架构,开销也各不相同的英特尔奔腾4,英特尔酷睿2双核和英特尔酷睿i7之间颇有几分。 开销甚至可能改变显着的Intel和AMD的CPU之间,即使双方在相同的时钟速度下运行,因为像高速缓存大小,缓存算法,内存访问模式和通话操作码的实际硬件实现本身可以有一个巨大的因素对开销的影响。

  2. 该ABI(应用程序二进制接口)。 即使是相同的CPU,往往存在指定功能如何调用传递参数(经由寄存器,通过堆叠,或通过两者的组合),并在何处以及如何堆栈帧初始化和清理发生不同的ABI。 这一切对开销的影响。 不同的操作系统可使用的相同的CPU不同的ABI; 如Linux,Windows和Solaris可能所有的三个使用不同的ABI相同的CPU。

  3. 编译器。 严格按照ABI如果函数被称为独立编码单元之间,例如,如果应用程序调用的系统库或用户库的一个函数调用另一个用户的库的功能是唯一重要的。 只要功能是“私人”,有一定的程序库或二进制外不可见,编译器可能会“欺骗”。 它可能不是严格遵循ABI而是使用导致更快的函数调用的捷径。 例如,它可以在寄存器传递参数,而不是使用堆叠的,或者它可以跳过堆栈帧的设置和清理完全如果不是真的必要。

如果你想知道上述三个因素的影响,例如英特尔酷睿i5在Linux上使用GCC的特定组合的开销,你唯一的办法来获取这些信息是标杆两个实现,一个使用函数调用和一个在那里你之间的区别直接将代码复制到该呼叫者; 这种方式可以强制内联肯定的,因为行内说法只是一个提示,并不总是导致内联。

然而,真正的问题是:有没有确切的开销真的重要吗? 有一两件事是肯定的:函数调用总是有一个开销。 这可能是小的,也可能是大的,但它是肯定存在的。 而且不管它是多么小,如果一个函数被调用的频率足以在性能关键部分,开销会重要到一定程度。 内联很少让你的代码慢,除非你非常过头; 它将使代码虽然大。 今天的编译器是在决定自己当内联和当不相当不错,所以你很少有机架一下你的大脑。

我个人的发展过程中忽略了内联完全,直到我有更多或更少的可用的产品,我可以配置文件,并只有在分析告诉我,某一个函数被调用真正往往也是该应用程序的性能关键部分内,那么我会考虑此功能的“力 - 内联”。

到目前为止,我的答案是很普通的,它仅针对C,也同样适用于C ++和Objective-C。 作为闭幕词让我说一些关于C ++尤其是:方法是虚拟的双重间接函数调用,这意味着他们有较高的函数调用的开销比普通的函数调用,也不能被内联。 非虚方法可能会被编译器内联或没有,但即使他们不内联,但仍比虚拟的显著快,所以你不应该做的方法虚拟的,除非你真的打算覆盖它们,或让他们重写。



Answer 5:

我对做一个简单的增量功能的简单的基准:

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;
}

与我的英特尔(R)酷睿(TM)i5的CPU中号430 @ 2.27GHz给了我一个十亿反复运行它:

  • 1.4秒inlinining版本
  • 4.4秒定期联系版本

(看来由波动高达0.2,但我懒得计算正确的标准偏差也不关心他们的)

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

最快的我测的东西它是约0.3ns所以这将提出一个函数调用的成本约9原始OPS,说得非常简单化。

此开销增加每呼叫约另一个为2ns(总时间呼叫时间大约为6ns),用于通过一个称为PLT(在一个共享库函数)的功能。



Answer 6:

对于很小的函数内联是有道理的,因为函数调用的(小)的成本相对于函数体的(很小)成本显著。 在过去的几行大部分功能它不是一个巨大的胜利。



Answer 7:

值得指出的是,内联函数会增加调用函数和任何增加的函数的大小可能会对缓存产生负面影响的大小。 如果你在边界“只是多了一个晶圆薄薄荷”的内联代码是对可能会对性能产生显着的负面影响。


如果你正在阅读文献指出的警告有关“函数调用的成本,”我建议这可能是因为没有体现出现代处理器的旧版材料。 除非你在嵌入式领域的时候,时代其中C是“便携式汇编语言”基本上已经过去了。 大量的芯片设计人员在过去十年的别出心裁的(比如说)已经进入各种低层次的复杂性,可以从事物的工作方式不同,根本“在当天回。”



Answer 8:

现代的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();
}

好像调用的开销是不可能减少的,因为它确实已经hoised功能的一大块圈外(在CalculatePi_1000_digits调用)。 编译器将需要能够证明CalculatePi_1000_digits总是返回相同的结果,但良好的优化器能做到这一点。



Answer 9:

有一个很大的概念被称为“寄存器遮蔽”,这允许通过(最多6?),直通寄存器(上CPU)代替堆(存储器)的值。 另外,根据中使用的函数和变量,编译器可能只是决定不要求框架管理代码!

此外,即使C ++编译器可以做一个“尾递归optimiztaion”,即,如果A()调用B(),并调用B(后),A刚刚返回,编译器将重用堆栈帧!!

当然,这一切都可以做到的,只要程序坚持以标准的语义(见指针别名,它是对优化效果)



Answer 10:

没有太多的开销可言,特别是小(直列能)功能,甚至类。

下面的例子有分别运行很多很多时间和定时三种不同的测试。 结果总是等于单位时间内的一对夫妇千分之一的顺序。

#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

给出相同的结果,除了规模的一对夫妇的订单每种情况下更快。



Answer 11:

每一个新的功能要求,以创建新的本地堆栈。 但如果你是在一个非常大的迭代次数的调用一个循环的每次迭代函数的这个开销只会是明显的。



Answer 12:

对于大多数功能,他们是没有额外的开销称他们在C ++ VS C(除非你认为“这个”指针作为不必要的争吵到每一个功能。你必须以某种方式传递状态的功能寿)...

对于虚函数,它们是额外的间接水平(相当于主叫通过在C指针的函数)......不过说真的,今天的硬件,这是微不足道的。



Answer 13:

我没有任何数字,要么,但我很高兴你问。 我经常看见人们试图优化其代码开头开销模糊的想法,但不真正了解。



Answer 14:

这里有几个问题。

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

  • 如果函数是虚拟的,那么你当然要付出的代价,由于目标在运行时确定它不能被内联。 相反,在Java中,你可能会除非你表明,该方法是最后的支付这个价格。

  • 根据你的代码是如何在内存中组织的,你可能会付出高速缓存未命中成本和偶数页错过的代码位于别处。 可最终不得不在某些应用中产生巨大的影响。



Answer 15:

这取决于你如何组织你的代码,划分单位,如模块和库它可能在某些情况下深刻的关系。

  1. 使用动态库函数具有外部链接将大部分时间强加完整的堆栈帧的处理。
    这就是为什么使用的qsort从STDC库大小(10倍)比使用STL代码时比较操作是作为整数比较简单慢一个数量级。
  2. 传递模块之间的函数指针也将受到影响。
  3. 同样的处罚将最有可能影响的C ++的虚拟功能的使用以及其他功能,其代码在单独的模块中定义。

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



Answer 16:

正如其他人所说,你真的不必过于担心开销,除非你打算为终极性能或某种类似。 当你犯了一个函数,编译器写代码:

  • 保存功能参数堆栈
  • 保存返回地址到堆栈
  • 跳转到函数的起始地址
  • 分配空间的函数的局部变量(堆栈)
  • 运行函数体
  • 保存返回值(栈)
  • 对于局部变量又名垃圾收集的可用空间
  • 跳回到保存的返回地址
  • 腾出保存的参数等...

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



文章来源: How much overhead is there in calling a function in C++?