我是一个虔诚的人,并努力不犯罪。这就是为什么我倾向于编写较小的功能(小于此值以重写罗伯特·C·马丁的话),以符合《清洁法》圣经所定的几条诫命。但是在检查某些内容时,我落在了这篇文章上,在下面阅读了以下评论:


请记住,方法调用的成本可能很高,具体取决于
语言。在编写
可读代码与编写性能代码之间几乎总要权衡取舍。


在当今条件下,考虑到高性能现代编译器行业的丰富,这种引用语句现在仍然有效吗?

这是我唯一的问题。这与我应该编写长函数还是小函数无关。我只是强调您的反馈可能会(或不会)有助于改变我的态度,并使我无法抵抗亵渎神灵的诱惑。

评论

编写可读且可维护的代码。仅当遇到堆栈溢出问题时,您才可以重新考虑spproach

这里的一般答案是不可能的。有太多不同的编译器,实现了太多不同的语言规范。然后是JIT编译的语言,动态解释的语言等等。不过,只要您使用现代的编译器编译本机C或C ++代码,就不必担心函数调用的开销了。优化器将在适当的时候内联这些。作为微优化的爱好者,我很少看到编译器做出内联的决定,而这些决定是我或我的基准测试所不同意的。

从个人经验上讲,我用一种专有的语言编写了代码,这种专有的语言在功能上相当现代,但是函数调用却非常昂贵,以至于甚至对于典型的for循环都必须优化速度:for(Integer index = 0, size = someList.size(); index
@phyrfox很有道理,在循环外获取someList.size()的值,而不是每次循环都调用它。如果在同步期间读者和编写者可能会尝试发生冲突,那么在出现同步问题的可能性时尤其如此,在这种情况下,您还希望保护列表免受迭代期间的任何更改。

注意不要将小功能带到太远,它可能像单片宏功能一样有效地混淆代码。如果您不相信我,请查看ioccc.org的一些获奖者:有些将所有代码编码到一个main()中,另一些将所有代码拆分成约50个小函数,而这些都是完全不可读的。诀窍是一如既往地保持良好的平衡。

#1 楼

这取决于您的领域。

如果您正在为低功耗微控制器编写代码,则方法调用成本可能会很高。但是,如果您要创建普通的网站或应用程序,则与其余代码相比,方法调用成本可以忽略不计。在这种情况下,始终将更多的精力放在正确的算法和数据结构上,而不是像方法调用之类的微优化上。

还有编译器为您内联方法的问题。大多数编译器足够智能,可以在可能的地方内联函数。

最后,性能的黄金法则是:始终优先考虑。不要基于假设编写“优化”代码。如果您不满意,请写两种情况,看看哪一种更好。

评论


和例如HotSpot编译器执行推测内联,在某种意义上,即使不可能也可以内联。

–Jörg W Mittag
17年9月9日在10:20

实际上,在Web应用程序中,相对于数据库访问和网络流量,整个代码可能无关紧要。

– AnoE
17年9月9日15:45

我实际上是通过一个非常老的编译器进入嵌入式和超低功耗的,该编译器几乎不了解优化的含义,并且即使函数调用很重要,也仍然相信我,这绝不是寻找优化的第一位。即使在这种特殊领域,在这种情况下,代码质量也是第一位。

– Tim
17年9月9日在18:49

好答案!您的最后一点应该是第一位:在确定最佳位置之前,始终先进行剖析。

– CJ Dennis
17年9月11日在2:32

另一方面,重要的是程序员在职业生涯的早期就定期学习使用探查器。这样,您将了解做什么和不该做什么。如果在职业生涯的早期形成了不良的编程习惯,就很难解决,这反过来可能会影响一个人的职业道路。虽然这也取决于企业文化。在某些工作环境中,上市时间更为重要。代码性能可能不那么重要。在这些环境中,可以快并打破事物。尽管甚至Facebook现在都在远离这一座右铭。

–rwong
17/09/11在4:41



#2 楼

函数调用开销完全取决于语言以及您在什么级别上进行优化。

在超低级别上,函数调用甚至更多,因此如果虚拟方法调用会导致分支预测错误或错误调用,则可能会导致代价高昂CPU缓存未命中。如果您已经编写了汇编程序,那么您还将知道需要一些额外的说明来保存和恢复调用周围的寄存器。 “足够聪明”的编译器能够内联正确的函数以避免这种开销是不正确的,因为编译器受语言语义的限制(尤其是围绕诸如接口方法分派或动态加载的库之类的功能)的限制。 />
在较高的层次上,Perl,Python,Ruby之类的语言在每个函数调用中都会做大量记账工作,这使得它们的成本相对较高。元编程使情况变得更糟。我曾经只是通过在非常热的循环中提升函数调用来加快Python 3x的速度。在对性能有严格要求的代码中,内联帮助函数可以产生显着效果。

但是,绝大多数软件对性能的要求并不那么严格,以至于您可以注意到函数调用的开销。无论如何,编写干净,简单的代码都可以得到回报:


如果您的代码不是性能至关重要的代码,则使维护更加容易。即使在性能至关重要的软件中,大多数代码也不会成为“热点”。
如果您的代码对性能至关重要,那么简单的代码将使您更容易理解代码并发现优化机会。通常,最大的胜利不是来自内联函数之类的微优化,而是算法的改进。或用不同的措辞:不要更快地完成相同的事情。找到减少工作的方法。

注意,“简单代码”并不意味着“分解为一千个小函数”。每个函数还引入了一些认知上的开销–推理更多抽象代码更加困难。在某些时候,这些微小的功能可能做得很少,以至于不使用它们会简化您的代码。

评论


一个非常聪明的DBA曾经告诉我“规范化直到感到痛苦,然后对规范化直到没有伤害为止”。在我看来,它可以改写为“提取方法,直到遇到麻烦为止,然后再进行内联直到找到为止。”

–RubberDuck
17年9月10日在16:08

除认知开销外,调试器信息中还存在符号性开销,通常最终二进制文件中的开销是不可避免的。

–弗兰克·希勒曼
17年9月10日在22:42

关于智能编译器-他们可以这样做,但并非总是如此。例如,jvm可以基于运行时配置文件以非常便宜/免费的陷阱来内联事物,以实现不常见的路径或内联多态函数,对于这些内联多态函数只有给定方法/接口的一种实现,然后在动态加载新的子类时将其调用优化为适当的多态运行。但是,是的,在许多语言中,这种事情是不可能的,甚至在jvm中,在许多情况下,这都不划算,或者在一般情况下是不可能的。

– Artur Biesiadowski
17年9月12日在11:15

#3 楼

关于性能调整代码的几乎所有格言都是阿姆达尔定律的特例。阿姆达尔定律的简短幽默描述是


如果程序的某个部分占用了5%的运行时,并且您对该程序进行了优化以使它现在占用了零%的运行时,则该程序整个过程只会提高5%。


(完全可以将运行时间降低到零%进行优化:当您坐下来优化大型,复杂的程序时,很可能发现它至少在部分不需要的东西上花费了它的运行时。)这就是为什么人们通常说不用担心函数调用成本:无论多么昂贵实际上,整个程序通常只花费其运行时间的一小部分用于调用开销,因此加快它们的使用并没有太大帮助。

但是,如果有一个技巧,您可以拉这样可以使所有函数的调用速度更快,这种技巧可能是值得的。编译器开发人员花费大量时间来优化函数“序言”和“结语”,因为这对使用该编译器编译的所有程序都有利,即使每个程序只有一点点。

而且,如果您有理由相信程序会花费大量运行时间来进行函数调用,那么您应该开始考虑这些函数调用中是否有一些是不必要的。这里有一些经验法则,可以帮助您了解何时应该执行此操作:


如果函数的每次调用运行时小于一毫秒,但该函数被称为数十万次,
如果程序的概要文件显示成千上万个函数,而它们中的任何一个都不占用运行时的0.1%左右,则函数调用开销可能合计很大。
如果您有“ lasagna代码”,其中有许多抽象层,除了分发到下一层几乎没有任何工作,并且所有这些层都是通过虚拟方法调用实现的,那么很有可能CPU浪费了间接分支管道停顿的时间很多。不幸的是,解决此问题的唯一方法是去除某些层,这通常非常困难。


评论


只是要提防在嵌套循环深处完成的昂贵工作。我优化了一个功能,并获得了运行速度快10倍的代码。那是在探查者指出罪魁祸首之后。 (在从O(n ^ 3)到小的n O(n ^ 6)的循环中反复调用它。)

–Loren Pechtel
17年9月11日下午4:01

“不幸的是,唯一的解决方法是摆脱某些层,这通常很难。” -这在很大程度上取决于您的语言编译器和/或虚拟机技术。如果可以修改代码以使编译器更容易内联(例如,通过使用适用于Java的最终类和方法,或者使用C#或C ++中的非虚拟方法),则可以通过编译器/运行时和如果不进行大规模重组,您将看到收获。正如@JorgWMittag指出的那样,在无法证明优化是...的情况下,JVM甚至可以内联。

–法律
17年9月12日在9:45



...有效,因此尽管存在分层,但很可能是在您的代码中进行的。

–法律
17年9月12日在9:47

@Jules虽然确实JIT编译器可以执行推测性优化,但这并不意味着这些优化是统一应用的。特别是关于Java,我的经验是,开发人员文化偏爱堆积在层之上的层,从而导致极深的调用堆栈。有趣的是,这导致了许多Java应用程序的呆滞,膨胀。这种高度分层的体系结构不利于JIT运行时,无论这些层在技术上是否可内联。 JIT并不是可以自动解决结构问题的灵丹妙药。

–阿蒙
17年9月12日在11:34

@amon我对“ lasagna代码”的经验来自于大型C ++应用程序,其代码可追溯到1990年代,当时深嵌套的对象层次结构和COM成为时尚。 C ++编译器做出了巨大的英勇努力,以消除此类程序中的抽象惩罚,但您仍然可能会看到,他们在间接分支流水线停顿上花费了大量的挂钟运行时(以及I缓存未命中的另一个重要块) 。

– zwol
17年9月17日在15:58

#4 楼

我会挑战这个报价:


在编写可读代码和
编写高性能代码之间几乎总是要取舍。


真正令人误解的陈述和潜在的危险态度。在某些特定情况下,您必须进行权衡,但通常两个因素是独立的。

一个必要权衡的示例是,您拥有简单的算法而不是更复杂但性能更高的算法。哈希表实现显然比链表实现更复杂,但是查找会更慢,因此您可能不得不为了性能而牺牲简单性(这是可读性的一个因素)。

关于函数调用的开销,根据算法和语言的不同,将递归算法转换为迭代算法可能会具有明显的优势。但这又是非常特定的情况,通常,函数调用的开销可以忽略不计或被优化。

(某些动态语言(例如Python)确实存在很大的方法调用开销。但是,如果性能变得一个问题,您可能首先不应该使用Python。)

可读代码的大多数原则-一致的格式,有意义的标识符名称,适当且有用的注释等对性能没有影响。还有一些(例如使用枚举而不是字符串)也具有性能上的好处。

#5 楼

在大多数情况下,函数调用开销并不重要。

但是,内联代码的最大好处是在内联之后优化新代码。

例如,如果您使用常量参数调用函数,则优化器现在可以在内联调用之前将参数折叠到无法折叠的位置。如果参数是函数指针(或lambda),那么优化器现在也可以内联对该lambda的调用。

这是虚拟函数和函数指针没有吸引力的主要原因,因为您无法内联除非实际的函数指针一直一直折叠到调用位置,否则它们都不会。

#6 楼

假设性能对您的程序很重要,并且确实有很多调用,那么根据调用的类型而定,成本仍然可能无关紧要。

如果被调用的函数很小,并且编译器能够内联它,那么代价将基本上为零。现代的编译器/语言实现具有JIT,链接时间优化和/或模块系统,旨在在有益时最大程度地内联函数。

OTOH,函数调用有一个显而易见的代价:它们的存在可能会限制调用之前和之后的编译器优化。

如果编译器无法推断被调用函数的功能(例如,虚拟/动态调度或动态库中的函数),则可能不得不悲观地假设该函数可以具有任何副作用-引发异常,修改全局状态或更改通过指针看到的任何内存。编译器可能必须将临时值保存到后台存储器,并在调用后重新读取它们。它无法围绕调用重新排序指令,因此它可能无法向量化循环或提升循环外的冗余计算。

例如,如果您不必要地在每个循环中调用一个函数循环迭代:

for(int i=0; i < /* gasp! */ strlen(s); i++) x ^= s[i];


编译器可能知道它是一个纯函数,并将其移出循环(在这种情况下,这种情况很糟糕,甚至修复了意外的O(n ^ 2)算法为O(n)):

for(int i=0, end=strlen(s); i < end; i++) x ^= s[i];


然后甚至可以重写该循环,以同时使用wide / SIMD指令。

但是,如果您在循环中添加对某些不透明代码的调用,即使该调用不执行任何操作且本身非常便宜,编译器也必须假设最坏的情况—该调用将访问指向与s更改其内容相同的内存的全局变量(即使函数中为const,也可以在其他任何地方为非const),从而无法进行优化:
for(int i=0; i < strlen(s); i++) {
    x ^= s[i];
    do_nothing();
}


#7 楼

这篇旧文章可能会回答您的问题:



小盖伊·路易斯·斯蒂尔。“揭穿“昂贵的程序调用”神话,或者程序调用实现被认为有害,或者,Lambda:The
Ultimate GOTO”。麻省理工学院AI实验室AI实验室备忘录AIM-443。 1977年10月。


摘要:


民俗学说GOTO语句“便宜”,而过程
调用则“昂贵”。 。这个神话很大程度上是由于
设计语言实现不当造成的。考虑了这个神话的历史发展。讨论了理论思想和现有的实现方法
,这颠覆了这个神话。结果表明,
不受限制地使用过程调用可带来极大的时尚自由。特别是,任何流程图都可以编写为“结构化”程序,而无需引入额外的变量。 GOTO
语句和过程调用的困难表现为抽象编程概念和具体语言
构造之间的冲突。


评论


我非常怀疑old会回答“函数调用成本在现代编译器中是否仍然重要”的问题。

–科迪·格雷
17年9月9日在16:58

@CodyGray我认为编译器技术自1977年以来应该已经发展起来。因此,如果可以在1977年使函数调用变得便宜,那么我们现在应该能够做到。所以答案是否定的。当然,这假设您使用的是体面的语言实现,可以执行诸如函数内联之类的事情。

– Alex Vong
2017年9月9日18:39



@AlexVong依靠1977年的编译器优化就像依赖石器时代的商品价格趋势。一切都改变了太多。例如,乘法曾经被廉价的操作所取代。目前,它的价格要昂贵得多。虚拟方法调用比以前要昂贵得多(内存访问和分支错误预测),但是通常可以对其进行优化,甚至可以内联虚拟方法调用(Java一直都在这样做),因此成本是正好为零。 1977年没有这样的事情。

– maaartinus
17/09/10在2:13



正如其他人指出的,不仅仅是编译器技术的变化使过去的研究无效。如果编译器在微体系结构基本保持不变的情况下继续改进,那么本文的结论将仍然有效。但是那没有发生。如果有的话,微体系结构的变化远不止编译器。相对来说,以前快的东西现在慢了。

–科迪·格雷
17年9月10日在11:59

@AlexVong要更精确地描述使该纸张过时的CPU更改:早在1977年,主内存访问是一个CPU周期。如今,即使是对L1(!)高速缓存的简单访问,其延迟也将达到3到4个周期。现在,函数调用在内存访问中非常繁琐(创建堆栈帧,保存返回地址,保存用于局部变量的寄存器),这很容易将单个函数调用的成本提高到20个甚至更多个周期。如果您的函数仅重新排列其参数,并且可能添加另一个常量参数以传递给直通,那么这几乎是100%的开销。

–cmaster-恢复莫妮卡
17年9月11日在12:54

#8 楼


在C ++中请注意设计用于复制参数的函数调用,默认值为“按值传递”。由于节省了寄存器和其他与堆栈框架相关的东西而导致的函数调用开销可能会因对象的意外复制(可能非常昂贵)而无法承受。
在放弃高度分解的代码之前,应该研究与堆栈框架相关的优化。
大多数时候,当我不得不处理一个缓慢的程序时,我发现进行算法更改可以提高速度比内联函数调用更容易。例如:另一位工程师重做了一个解析器,该解析器填充了map-of-maps结构。作为其一部分,他从一个映射中删除了一个缓存索引到一个逻辑关联的索引。这是一个很好的代码健壮性,但是由于对所有将来的访问进行哈希查找而不是使用存储的索引,导致程序减速了100倍,从而使程序无法使用。分析表明,大部分时间都花在了散列函数上。


评论


第一条建议有些陈旧。从C ++ 11开始,移动成为可能。特别是,对于需要在内部修改其参数的函数,按值获取参数并就地对其进行修改可能是最有效的选择。

– MSalters
17年9月11日在6:45

@MSalters:我认为您特别误以为“更多”或其他内容。传递副本或引用的决定是在C ++ 11之前做出的(据我所知您知道)。

–菲涅耳
17年9月12日在10:36

@phresnel:我认为我做对了。我所指的特殊情况是您在调用方中创建一个临时项,将其移至一个参数,然后在被调用方中对其进行修改。在C ++ 11之前这是不可能的,因为C ++ 03无法/不会将非常量引用绑定到临时对象。

– MSalters
17年9月12日在12:50

@MSalters:然后,我在第一次阅读它时误解了您的评论。在我看来,您的意思是,在C ++ 11之前,如果要修改所传递的值,则按值传递不是一件事。

–菲涅耳
17年9月12日在12:54

“移动”的到来最有助于返回对象,该对象在函数中比在外部构造更方便,并且通过引用传递。在此之前,从函数返回对象会调用副本,这通常是昂贵的举动。那不涉及函数参数。我仔细地在注释中加上了“设计”一词,因为必须明确给予编译器许可以“移入”函数自变量(&&语法)。我已经习惯了“删除”副本构造函数,以找出这样做有价值的地方。

–user2543191
17年9月12日在14:05

#9 楼

是的,在现代硬件上错过分支预测的成本要比几十年前高,但是编译器在优化此功能方面变得更加聪明。

例如,考虑一下Java。乍一看,函数调用开销在该语言中应该特别占优势:


由于JavaBean约定,微小的函数非常普遍。
函数默认为virtual,通常是
/>编译单元是类;运行时支持随时加载新类,包括覆盖以前的单态方法的子类。

由于这些实践的困扰,普通的C程序员会预测Java必须比C慢至少一个数量级。而20年前,他本来是对的。但是,现代基准测试将惯用的Java代码放在等效C代码的百分之几之内。

原因之一是现代JVM内联函数调用是理所当然的事情。它使用推测性内联来实现:


新加载的代码无需优化即可执行。在此阶段,对于每个调用站点,JVM都会跟踪实际调用了哪些方法。
一旦代码被确定为性能热点,运行时就会使用这些统计信息来识别最可能的执行路径,并内联一个,在不应用推测性优化的情况下,以条件分支为前缀。

即,代码:

int x = point.getX();


当然,运行时足够聪明,只要未分配点,就可以向上进行此类型检查;如果调用者知道该类型,则取消它代码。

总而言之,即使Java都管理自动方法内联,也没有内在的理由解释为什么编译器不支持自动内联,并且没有任何理由这样做,因为内联在现代处理器上非常有用。因此,我几乎无法想象有任何现代主流编译器不了解这种最基本的优化策略,除非有其他证明,否则我会假设有能力做到这一点。

评论


“没有编译器不支持自动内联的内在原因” –存在。您已经谈到了JIT编译,它相当于自我修改的代码(由于安全性,操作系统可能会阻止该代码)以及执行自动配置文件引导的完整程序优化的能力。用于允许动态链接的语言的AOT编译器了解不足,无法虚拟化和内联任何调用。 OTOH:AOT编译器有时间优化它可以做的所有事情,JIT编译器只有时间专注于热点的廉价优化。在大多数情况下,这使JIT处于劣势。

–阿蒙
17年9月10日在19:41

告诉我一个操作系统,因为“安全性”而阻止运行Google Chrome(V8在运行时将JavaScript编译为本机代码)。另外,内联AOT并不是一个固有的原因(它不是由语言决定的,而是由您为编译器选择的体系结构确定的),尽管动态链接确实会禁止跨编译单元进行AOT内联,但不会抑制编译内联大部分通话发生的单位。实际上,在使用动态链接的语言比Java少使用的语言中,有用的内联可以说容易得多。

– Meriton
17-09-10 23:39



值得注意的是,iOS阻止了非特权应用程序的JIT。 Chrome或Firefox必须使用Apple提供的网络视图,而不是自己的引擎。好的一点是,AOT与JIT是实现级别的,而不是语言级别的选择。

–阿蒙
17年9月11日在6:06

@meriton Windows 10 S和视频游戏机操作系统也倾向于阻止第三方JIT引擎。

–达米安·耶里克(Damian Yerrick)
17年9月11日在19:49

#10 楼

就像其他人所说的那样,您应该首先衡量程序的性能,并且在实践中可能不会发现任何差异。

从概念上讲,我仍然认为我会清除与您的程序混淆的一些内容题。首先,您会问:


在现代编译器中函数调用成本仍然重要吗?


注意关键字“ function”和“ compilers” 。您的报价稍有不同:


请记住,根据语言的不同,方法调用的成本可能很高。


关于方法,从面向对象的角度讲。

虽然“功能”和“方法”通常可以互换使用,但是在成本(您要询问的)和何时使用方面存在差异。涉及到编译(这是您提供的上下文)。

特别地,我们需要了解静态调度与动态调度。我暂时将忽略优化。

在像C这样的语言中,我们通常使用静态调度来调用函数。例如:

int foo(int x) {
  return x + 1;
}

int bar(int y) {
  return foo(y);
}

int main() {
  return bar(42);
}


当编译器看到调用foo(y)时,它知道foo名称所指的是什么函数,因此输出程序可以直接跳转到foo函数,这是相当便宜的。这就是静态调度的意思。

替代方法是动态调度,其中编译器不知道正在调用哪个函数。作为示例,下面是一些Haskell代码(因为C等效项很杂乱!):

foo x = x + 1

bar f x = f x

main = print (bar foo 42)


这里bar函数正在调用其参数f,可以是任何东西。因此,编译器不能只将bar编译为快速跳转指令,因为它不知道跳转到哪里。相反,我们为bar生成的代码将取消对f的引用,以找出其指向的功能,然后跳转到该功能。这就是动态调度的意思。

这两个示例都是针对功能的。您提到了方法,可以将其视为动态调度函数的一种特殊样式。例如,下面是一些Python:

class A:
  def __init__(self, x):
    self.x = x

  def foo(self):
    return self.x + 1

def bar(y):
  return y.foo()

z = A(42)
bar(z)


y.foo()调用使用动态调度,因为它在foo对象中查找y属性的值,并对其进行任何调用发现它不知道y将具有A类,或者A类包含foo方法,因此我们不能直接跳转到它。

好,这是基本思想。注意,无论我们编译还是解释,静态调度都比动态调度快。其他所有条件都一样。无论哪种方式,取消引用都会产生额外的费用。

那么这如何影响现代的,优化的编译器呢?

首先要注意的是,可以更加优化静态分派:当我们知道我们要跳转到哪个功能时,就可以执行内联等操作。使用动态调度,我们不知道要等到运行时才跳转,因此我们无法做很多优化。

其次,在某些语言中,可以推断出某些动态调度将在何处结束跳转并因此将它们优化为静态调度。这使我们可以执行其他优化,例如内联等。

在上面的Python示例中,这种推断是毫无希望的,因为Python允许其他代码覆盖类和属性,因此很难推断出将保留的内容在所有情况下。

如果我们的语言允许我们施加更多限制,例如通过使用注释将y限制为A类,那么我们可以使用该信息来推断目标函数。在具有子类化的语言(几乎是所有具有类的语言!)中,这实际上是不够的,因为y实际上可能具有不同的(子)类,因此我们需要Java的final批注之类的额外信息才能确切知道将调用哪个函数。

Haskell不是OO语言,但我们可以通过将f(静态分派)内联到bar中(用main代替foo)来推断y的值。由于foo中的main的目标是静态已知的,因此该调用将被静态分派,并且可能会内联和完全优化(由于这些函数很小,编译器更可能内联它们;尽管我们不能指望它们)一般而言)。

因此费用降低到:


语言是静态还是动态分派您的呼叫?
如果是后者,该语言是否允许实现使用其他信息(例如类型,类,注释,内联等)来推断目标?
如何积极地优化(推断或其他方式的)静态派发?

如果您使用的是“非常动态”的语言,并且动态分配很多,编译器无法保证,那么每次调用都会产生成本。如果您使用的是“非常静态”的语言,那么成熟的编译器将产生非常快速的代码。如果介于两者之间,则可能取决于您的编码样式以及实现的聪明程度。

评论


我不同意像您的Haskell示例一样,调用闭包(或某些函数指针)是动态调度。动态分配涉及一些计算(例如使用某些vtable)来获得该关闭,因此比间接调用的开销更大。否则,很好的答案。

–Basile Starynkevitch
17年12月1日在11:46



#11 楼


请记住,方法调用的成本可能很高,具体取决于语言。不幸的是,在编写可读代码和编写高性能代码之间几乎总是要取舍。


高度依赖于:


编译器工具链,包括JIT(如果有的话)。



首先,性能优化的第一定律是配置文件优先。在许多领域中,软件部分的性能与整个堆栈的性能均不相关:数据库调用,网络操作,OS操作,...

这确实意味着软件的性能完全无关紧要,即使它不会改善延迟,优化软件也可能会节省能源和硬件(或节省移动应用程序的电池),这很重要。
眼见为实,并且经常算法改进会大大超越微观优化。

因此,在进行优化之前,您需要了解要进行的优化...以及是否值得。


现在,就纯软件性能而言,工具链之间的差异很大。

函数调用有两个成本:


运行时成本,
编译时成本。

运行时成本相当明显;为了执行功能调用,必须进行一定量的工作。例如,在x86上使用C,函数调用将需要(1)将寄存器溢出到堆栈中,(2)将参数推入寄存器,执行调用,然后(3)从堆栈中恢复寄存器。请参阅此调用约定摘要以了解所涉及的工作。

此寄存器溢出/恢复需要很短的时间(数十个CPU周期)。

通常预计,与执行该功能的实际成本相比,此成本将是微不足道的,但是此处有些模式适得其反:吸气剂,受简单条件保护的函数等...

因此,除了解释器之外,程序员还希望他们的编译器或JIT将优化不必要的函数调用。尽管这种希望有时可能不会见效。因为优化器不是魔术师。

优化器可以检测到函数调用是微不足道的,并且可以内联该调用:本质上是在调用站点复制/粘贴函数的主体。这并不总是一个好的优化(可能会导致膨胀),但总的来说是值得的,因为内联公开了上下文,并且上下文启用了更多的优化。

一个典型的示例是:

void func(condition: boolean) {
    if (condition) {
        doLotsOfWork();
    }
}

void call() { func(false); }


如果内联了func,则优化器将意识到该分支永远不会被占用,并将call优化为void call() {}

在这种意义上,函数通过隐藏信息来调用来自优化器的信息(如果尚未内联)可能会抑制某些优化。虚函数调用尤其如此,因为去虚拟化(证明最终在运行时调用哪个函数)并不总是那么容易。


总而言之,我的建议是首先写清楚,避免过早的算法悲观化(立方复杂度或较差的咬合迅速),然后仅优化需要优化的内容。

#12 楼


“请记住,根据语言的不同,方法调用的成本可能很高。
在编写
可读代码和编写性能代码之间几乎总是要权衡取舍。”

在当今条件如何的情况下,考虑到高性能现代编译器的丰富行业,今天引用的语句仍然有效吗?


我只是断然拒绝。我相信引用只是随便丢掉就可以了。

当然,我并没有讲完整的事实,但是我不太在乎那么真实。就像在那部Matrix电影中,我忘了它是1还是2或3-我认为那是性感的意大利女演员和大甜瓜(除了第一个,我真的不喜欢)的那个时候,甲骨文女士告诉基努·里夫斯,“我只是告诉你你需要听的东西”,或者类似的事情,这就是我现在想要做的。

程序员不需要听这些。如果他们有使用分析器的经验,并且报价在某种程度上适用于他们的编译器,那么他们将已经知道这一点,并且将通过了解它们的分析输出,以及通过测量来了解为什么某些叶子调用是热点的正确方法,来学习此方法。如果他们没有经验并且从未描述过他们的代码,这是他们需要听到的最后一件事,那就是他们应该开始迷信地破坏他们的代码编写方式,直到发现热点之前都将它们内联,以期希望它能够变得更有表现。

无论如何,为了获得更准确的响应,这取决于。某些条件已经列出了很好的答案。仅选择一种语言的可能条件本身已经非常庞大,例如C ++,必须在虚拟调用中进入动态调度,何时可以对其进行优化,以及在何种条件下可以进行编译器甚至是链接器,并且已经保证了详细的响应,更不用说尝试了。以各种可能的语言解决条件,并在那里编译。但我会在最上面加上“谁在乎?”因为即使在性能至关重要的领域(例如光线追踪)中工作,我也要开始做的最后一件事是在进行任何测量之前手动插入方法。

我确实相信有些人对建议并不热衷在测量之前,切勿进行任何微优化。如果将参考计数的局部性优化视为微优化,那么我经常确实会从一开始就以面向数据的设计思路开始在某些我认为对性能至关重要的领域(例如,光线跟踪代码)应用此类优化,因为否则,我知道在这些领域工作多年后,必须尽快重写大部分内容。除非我们像二次时间一样线性讨论,否则针对缓存命中而优化数据表示通常可以具有与算法改进相同的性能改进。测量,尤其是因为探查器很擅长于揭示内联可能带来的好处,而不是揭示未被内联带来的好处(并且如果没有内联函数的调用很少见,那么不进行内联实际上可以使代码更快,从而提高了引用的局部性)用于热代码的icache,有时甚至允许优化器针对常见的执行路径做得更好。)