在C ++中调用函数有多less开销?
很多文献谈到使用内联函数来“避免函数调用的开销”。 但是我还没有看到可以量化的数据。 函数调用的实际开销是多less?通过内联函数,我们达到了什么样的性能提升?
在大多数体系结构中,成本包括将全部(或部分或全部)寄存器保存到堆栈,将函数参数压入堆栈(或将其放入寄存器),递增堆栈指针并跳转到新的代码。 然后,当function完成后,你必须从堆栈中恢复寄存器。 这个网页描述了各种调用约定中涉及的内容。
大多数C ++编译器现在已经足够聪明来为你内联函数了。 inline关键字只是编译器的提示。 有些甚至会在翻译部门进行内联,他们认为这对他们有帮助。
有技术和实际的答案。 实际的答案是,它永远不会有问题,在极less数情况下,它是唯一的方法,你会知道是通过实际的configurationtesting。
由于编译器的优化,你的文献所引用的技术答案通常是不相关的。 但是如果你还有兴趣的话, 乔希就很好地描述过了。
就“百分比”而言,你必须知道这个函数本身的价格是多less。 被调用函数的成本之外,没有百分比,因为您正在比较零成本操作。 对于内联代码没有成本,处理器只是移动到下一条指令。 inling的缺点是更大的代码大小,与堆栈构build/拆卸成本不同,体现了其成本。
开销的大小取决于编译器,CPU等。开销的百分比取决于你内嵌的代码。 要知道的唯一方法是把你的代码和configuration文件两种方式 – 这就是为什么没有明确的答案。
你的问题是其中一个问题,没有人可以称之为“绝对真理”。 正常函数调用的开销取决于三个因素:
-
CPU。 x86,PPC和ARM CPU的开销差别很大,即使只停留在一种架构下,Intel Pentium 4,Intel Core 2 Duo和Intel Core i7之间的开销也不尽相同。 Intel和AMD CPU之间的开销可能会有显着的变化,即使两者都以相同的时钟速度运行,因为诸如高速caching大小,cachingalgorithm,内存访问模式和调用操作码本身的实际硬件实现等因素可能会有巨大的影响开销。
-
ABI(应用程序二进制接口)。 即使使用相同的CPU,通常也会有不同的ABI指定函数调用如何传递参数(通过寄存器,通过堆栈或通过两者的组合)以及堆栈帧初始化和清理在何处以及如何进行。 所有这些对开销都有影响。 不同的操作系统可能为同一个CPU使用不同的ABI; 例如,Linux,Windows和Solaris都可以为同一个CPU使用不同的ABI。
-
编译器。 严格遵循ABI只有在独立代码单元之间调用函数时才是重要的,例如,如果应用程序调用系统函数库或用户函数库调用另一个用户函数库的函数。 只要函数是“私有的”,在某个库或二进制文件之外不可见,编译器就可能“作弊”。 它可能不会严格遵循ABI,而是使用导致更快的函数调用的快捷方式。 例如,它可能会传递参数在寄存器而不是使用堆栈,或者如果不是真的必要的话,它可能会跳过堆栈帧设置和清理。
如果您想了解上述三个因素的特定组合的开销,例如对于使用GCC的Linux上的Intel Core i5,获取此信息的唯一方法是基准testing两个实现之间的差异,一个使用函数调用,另一个使用函数调用将代码直接复制到调用者; 这样你强制内联,因为内联语句只是一个提示,并不总是导致内联。
但是,真正的问题在于:确切的开销真的很重要吗? 有一点是肯定的:函数调用总是有开销。 它可能很小,可能很大,但肯定是存在的。 而且,如果在性能关键部分经常调用一个函数的话,无论多小,开销都会在一定程度上影响。 内联很less让你的代码变慢,除非你非常过分; 它会使代码更大。 今天的编译器非常擅长决定什么时候内联,什么时候内联,所以你几乎不用费脑筋就可以了。
就个人而言,我完全忽略了开发中的内联,直到我有一个或多或less可用的产品,我可以剖析,只有在剖析告诉我,某个函数经常被调用,而且在应用程序的性能关键部分,那么我会考虑这个function的“强制内联”。
到目前为止,我的答案是非常通用的,它适用于C和C ++和Objective-C一样。 作为结束语让我特别说一些关于C ++的东西:虚方法是双重间接函数调用,这意味着它们比普通的函数调用有更高的函数调用开销,也不能被内联。 非虚方法可能被编译器内联或不内联,即使它们没有被内联,它们仍然比虚方法快得多,所以你不应该使方法变成虚拟的,除非你真的打算覆盖它们或者重写它们。
我对一个简单的增量函数做了一个简单的基准:
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版本为1.4秒
- 定期连接版本为4.4秒
(它似乎波动高达0.2,但我懒得计算适当的标准差也不关心他们)
这表明在这台计算机上函数调用的开销大约是3纳秒
我测量的速度最快的是大约0.3ns,这意味着一个函数的调用花费了大约9个原始操作 ,简单来说就是这样。
对于通过PLT调用的函数(共享库中的函数),每次调用的开销增加了大约2ns (总调用时间约6ns )。
对于非常小的函数内联是有意义的,因为函数调用的(小)成本相对于函数体的(非常小的)成本是很重要的。 对于大多数function来说,这不是一个大的胜利。
值得指出的是,内联函数增加了调用函数的大小,任何增加函数大小的东西都可能对高速caching产生负面影响。 如果你是在一个边界的话,“再多一个薄薄的薄荷”代码可能会对性能产生巨大的负面影响。
如果您正在阅读关于“函数调用的成本”的文章,那么我认为它可能是旧版材料,并不反映现代处理器。 除非你处于embedded式世界,否则C是“便携式汇编语言”的时代已经基本上过去了。 在过去的十年里,芯片devise师的大量聪明才智已经进入各种低级的复杂性,这些复杂性可能与事情“回到当天”的方式有着根本的不同。
有一个很好的概念叫做“寄存器映射”(register shadowing),允许通过寄存器(在CPU上)而不是栈(存储器)来传递(最多6?)值。 另外,根据内部使用的函数和variables,编译器可能会决定不需要帧pipe理代码!
而且,即使C ++编译器也可以做'尾recursion优化',也就是说如果A()调用B(),并且在调用B()之后,A刚刚返回,编译器将会重用堆栈帧!
当然,这一切都可以完成,只有当程序坚持标准的语义(请参阅指针别名,它对优化的影响)
现代的CPU非常快(显然!)。 几乎每个涉及调用和parameter passing的操作都是全速指令(间接调用可能会稍微昂贵些,大多数是通过循环的第一次)。
函数调用的开销很小,只有调用函数的循环才能使调用开销相关。
因此,当我们今天讨论(和度量)函数调用的开销时,我们通常谈论的是不能将常见的子expression式提取出循环的开销。 如果一个函数在每次被调用的时候都要做一堆(相同的)工作,编译器将能够将其“提升”出循环,如果被内联,则会执行一次。 当不内联时,代码可能会继续前进,重复工作,你告诉它!
内联函数似乎不可能快得多,不是因为调用和参数的开销,而是因为可以从函数中提出的常见的子expression式。
例:
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(); }
看起来调用开销不可能被减less了,因为它确实使得循环中的大部分function(CalculatePi_1000_digits调用)成为可能。 编译器需要能够certificateCalculatePi_1000_digits总是返回相同的结果,但好的优化器可以做到这一点。
根本没有太多的开销,特别是对于小的(可内联的)函数甚至类。
以下示例有三个不同的testing,每个testing运行多次,多次和定时。 结果总是等于一个单位时间的几十分之一的顺序。
#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次迭代(每种types:简单,六次函数调用,三次对象调用)的输出是用这个半复杂的工作负载:
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
给出相同的结果,除了每个案例几个数量级的速度。
每个新function都需要创build一个新的本地堆栈。 但是,如果你在循环的每一次迭代中调用一个函数来进行大量的迭代,那么这个开销就会很明显。
对于大多数函数来说,在C ++和C语言中调用它们没有额外的开销(除非你把“this”指针作为每个函数的不必要的参数。你必须以某种方式将状态传递给函数)…
对于虚拟函数来说,它们是一个额外的间接级别(相当于通过C中的指针调用一个函数)。但实际上,在今天的硬件上这是微不足道的。
我也没有任何数字,但我很高兴你问。 我经常看到人们试图优化他们的代码,以开销的含糊的想法开始,但并不知道。
这里有几个问题。
-
如果你有一个足够聪明的编译器,即使你没有指定内联,它也会为你自动内联。 另一方面,有很多东西是不能内联的。
-
如果这个函数是虚拟的,那么当然你要付出的代价是它不能被内联,因为目标是在运行时确定的。 相反,在Java中,除非您指明方法是最终的,否则您可能会支付这个价格。
-
根据您的代码在内存中的组织方式,您可能会因为代码位于其他地方而导致caching未命中甚至页面丢失的代价。 这可能会在某些应用程序中产生巨大的影响。
根据你的代码结构的不同,划分成单元,比如模块和库,在某些情况下可能会非常重要。
- 使用dynamic库函数与外部链接将大部分时间强加全栈处理。
这就是为什么在比较操作和整数比较一样简单的情况下,使用stdc库中的qsort比使用stl代码慢一个数量级(10倍)的原因。 - 传递模块之间的函数指针也会受到影响。
-
同样的惩罚很可能会影响C ++的虚拟函数以及其他函数的使用,这些函数的代码是在单独的模块中定义的。
-
好消息是,整个程序优化可能解决静态库和模块之间的依赖问题。
正如其他人所说的那样,除非你要追求极致的performance或类似的东西,否则你不必担心太多的开销。 当你编译一个函数时,编译器必须编写代码来:
- 将函数参数保存到堆栈
- 将返回地址保存到堆栈
- 跳转到函数的起始地址
- 为函数的局部variables(堆栈)分配空间
- 运行函数的主体
- 保存返回值(堆栈)
- 可用空间的局部variablesaka垃圾回收
- 跳回到保存的返回地址
- 释放参数等保存…
但是,您必须考虑降低代码的可读性,以及它如何影响您的testing策略,维护计划和src文件的整体大小影响。