“函数调用很昂贵”与“保持函数较小”

"Function calls are expensive" vs. "Keep functions small"

本文关键字:函数 函数调用      更新时间:2023-09-26

一方面,我读到或听到"函数调用很昂贵",它们会影响效率(例如,在尼古拉斯·扎卡斯(Nicholas Zakas)的谷歌技术演讲中)。

然而,另一方面,似乎可以接受的是,函数/方法最好保持简短,并且应该只真正执行一项任务,正如这里普遍接受的那样。

我在这里错过了什么,还是这两个建议不是相互矛盾?是否有一些经验法则可以让人保持禅宗般的平衡?

适用于所有语言的一般规则是:保持函数(方法、过程)尽可能小。当您添加正确的命名时,您将获得非常可维护和可读的代码,您可以在其中轻松关注一般图片并向下钻取到有趣的细节。使用一种巨大的方法,您总是在查看细节,而大局是隐藏的。

此规则特别适用于聪明的语言和编译器,它们可以进行花哨的优化,例如内联或发现哪些方法不是真正的虚拟方法,因此不需要双重调度。

回到 JavaScript - 这在很大程度上依赖于 JavaScript 引擎。在某些情况下,我希望体面的引擎能够内联运行,避免执行成本,尤其是在紧密循环中。但是,除非您遇到性能问题,否则首选较小的函数。可读性更为重要。

在一个完美的世界里,没有错误(因为代码只是神奇地修复自己),并且需求从第一天起就被冻结了,有可能拥有巨大的全能功能。

但在这个世界上,它变得太贵了——而且不仅仅是在"人月"方面。尼古拉斯·扎卡斯(Nicholas Zakas)写了一篇精彩的文章,描述了当今软件开发人员面临的大多数挑战。

这种转变可能看起来有些人为,但我的观点是,"一个函数 - 一个任务"的方法更易于维护和灵活 - 换句话说,它最终使开发人员和客户都满意。

但是,这并不意味着

您不会努力使用尽可能少的函数调用:请记住,这不是重中之重。

我的经验法则是,如果一个函数超过一个充满线条的屏幕,那么是时候将它分解成更小的部分了,尽管我的许多函数自然而然地最终会比这小一些,而不会被"人为地"拆分。而且我通常会留下足够的空白,即使满屏也不是很多代码。

我尝试让每个函数只执行一项任务,但一个任务可能是"重新绘制屏幕",这将涉及一系列在单独函数中实现的子任务,而这些子任务又可能在单独的函数中有自己的子任务。

从感觉自然(对我来说)的可读性(因此易于维护)开始,我不担心函数调用会很昂贵,除非一段特定的代码在测试时表现不佳 - 然后我会考虑让事情重新内联(特别是在循环中,从嵌套循环开始)。虽然话虽如此,有时你只是知道一段特定的代码不会表现良好,并在进行测试之前重写它......

我会避免"过早优化",特别是对于使用智能编译器的语言,这些编译器可能会在幕后进行相同的优化。当我第一次开始使用 C# 时,有人告诉我,由于 JIT 编译器的工作方式,将代码分解为较小的函数在运行时成本更低

回到我的一个全屏规则,在 JavaScript 中,嵌套函数是很常见的(由于 JS 闭包的工作方式),这会使包含函数比我使用另一种语言时想要的更长,所以有时最终结果是妥协。

致所有人:这更像是"评论"。承认。我选择使用"答案"的空间。请容忍。

@StefanoFratini:请把我的笔记看成是建立在你工作的基础上的。我想避免批评。

以下是进一步改进帖子代码的两种方法:

  • 使用来自 process.hrtime() 的元组的两半。它返回一个数组 [秒,纳秒]。您的代码使用元组的纳秒部分(元素 1),我找不到它使用秒部分(元素 0)。
  • 明确单位。

我能匹配我的咆哮吗?邓诺。这是Stephano代码的发展。它有缺陷;如果有人告诉我这件事,我不会感到惊讶。那也没关系。

"use strict";
var a = function(val) { return val+1; }
var b = function(val) { return val-1; }
var c = function(val) { return val*2 }
var time = process.hrtime();
var reps = 100000000
for(var i = 0; i < reps; i++) { a(b(c(100))); }
time = process.hrtime(time)
let timeWith = time[0] + time[1]/1000000000
console.log(`Elapsed time with function calls: ${ timeWith } seconds`);
time = process.hrtime();
var tmp;
for(var i = 0; i < reps; i++) { tmp = 100*2 - 1 + 1; }
time = process.hrtime(time)
let timeWithout = time[0] + time[1]/1000000000
console.log(`Elapsed time without function calls: ${ timeWithout } seconds`);
let percentWith = 100 * timeWith / timeWithout
console.log(`'nThe time with function calls is ${ percentWith } percent'n` +
    `of time without function calls.`)
console.log(`'nEach repetition with a function call used roughly ` +
        `${ timeWith / reps } seconds.` +
    `'nEach repetition without a function call used roughly ` +
        `${ timeWithout / reps } seconds.`)

它显然是斯蒂芬诺代码的后代。结果大不相同。

Elapsed time with function calls: 4.671479346 seconds
Elapsed time without function calls: 0.503176535 seconds
The time with function calls is 928.397693664312 percent
of time without function calls.
Each repetition with a function call used roughly 4.671479346e-8 seconds.
Each repetition without a function call used roughly 5.0317653500000005e-9 seconds.

像Stephano一样,我使用了Win10和Node(对我来说是v6.2.0)。

我承认以下论点:

  • "从角度来看,在纳秒(十亿分之一,1e-9)内,光传播大约12英寸。
  • "我们只谈论少量纳秒(47比5),所以谁在乎百分比呢?"
  • "有些算法每秒都会进行无数次函数调用,所以它们加起来。
  • "我们大多数开发人员都不使用这些算法,因此担心函数调用的数量对我们大多数人来说适得其反。

我会在经济论点上挂帽子:我的电脑和之前的电脑每一台售价不到400美元(美国)。如果一个软件工程师每小时的收入大约在90到130美元之间,那么他们给老板的时间价值是一台像我这样的计算机与他们工作三四个小时的比例。在该环境中:

这与公司需要的软件停止工作时每小时损失的美元相比如何?

当付费客户暂时无法使用业务合作伙伴生产的收缩包装软件时,这与失去的商誉和声望相比如何?

还有很多其他这样的问题。我会省略它们。

正如我解释答案的那样:可读性和可维护性支配着计算机性能。我的建议?相应地编写代码的第一个版本。我尊敬的许多人都说短函数有帮助。

一旦你完成了代码并且不喜欢性能,找到瓶颈。我尊敬的许多人都说,这些观点从来都不是你所期望的。当你认识他们时,就工作他们。

所以双方都是对的。一些。

我?我猜我在某个地方。两分钱。

函数调用总是很昂贵(尤其是在循环中),并且内联不会像您想象的那样经常发生

Node.js(任何版本)附带的V8引擎应该广泛地内联,但实际上这种能力受到很大限制。

以下(琐碎的)代码片段证明了我的观点(Win10x64 上的节点 4.2.1)

"use strict";
var a = function(val) {
  return val+1;
}
var b = function(val) {
  return val-1;
}
var c = function(val) {
  return val*2
}
var time = process.hrtime();
for(var i = 0; i < 100000000; i++) {
  a(b(c(100)));
}
console.log("Elapsed time function calls: %j",process.hrtime(time)[1]/1e6);
time = process.hrtime();
var tmp;
for(var i = 0; i < 100000000; i++) {
  tmp = 100*2 + 1 - 1;
}
console.log("Elapsed time NO function calls: %j",process.hrtime(time)[1]/1e6);

结果

Elapsed time function calls: 127.332373
Elapsed time NO function calls: 104.917725

+/- 20% 性能下降

人们会期望 V8 JIT 编译器内联这些函数,但实际上abc可以在代码中的其他地方调用,并且不适合使用 V8 获得的唾手可得的果实内联方法

我见过很多代码(Java,Php,Node.js)由于方法或函数调用滥用而在生产中性能不佳:如果您编写代码套娃样式,运行时性能将随着调用堆栈大小线性下降,尽管看起来概念上很干净。