【问题标题】:Is not having local functions a micro optimisation?没有局部功能是一种微优化吗?
【发布时间】:2011-01-20 13:18:14
【问题描述】:

将内部函数移到这个函数之外,以便每次调用函数时都不会创建它是一种微优化吗?

在这种特殊情况下,doMoreStuff 函数仅在 doStuff 内部使用。我应该担心有这样的本地函数吗?

function doStuff() {
    var doMoreStuff = function(val) {
         // do some stuff
    }

    // do something
    for (var i = 0; i < list.length; i++) {
         doMoreStuff(list[i]);
         for (var  j = 0; j < list[i].children.length; j++) {
              doMoreStuff(list[i].children[j]);
         }
    }
    // do some other stuff

}

一个实际的例子是:

function sendDataToServer(data) {
    var callback = function(incoming) {
         // handle incoming
    }

    ajaxCall("url", data, callback);

} 

【问题讨论】:

  • 可能。但也有其他注意事项 - 在本例中,您通过内部函数创建的闭包非常宝贵,它允许您封装所有其他局部变量,而无需将它们作为参数传递或重新处理。

标签: javascript micro-optimization


【解决方案1】:

不确定这是否属于“微优化”类别。我会说不。

但这取决于您拨打doStuff 的频率。如果你经常调用它,那么一遍又一遍地创建函数是没有必要的,而且肯定会增加开销。

如果您不想在全局范围内使用“辅助函数”但避免重新创建它,您可以像这样包装它:

var doStuff = (function() {
    var doMoreStuff = function(val) {
         // do some stuff
    }
    return function() {
        // do something
        for (var i = 0; i < list.length; i++) {
            doMoreStuff(list[i]);
        }
        // do some other stuff 
    }
}());

由于返回的函数是一个闭包,它可以访问doMoreStuff。请注意,外部函数会立即执行((function(){...}()))。

或者您创建一个包含对函数的引用的对象:

var stuff = {
    doMoreStuff: function() {...},
    doStuff: function() {...}
};

更多关于封装、对象创建模式和其他概念的信息可以在JavaScript Patterns一书中找到。

【讨论】:

  • 这是一个非常有趣的概念。使用闭包使其表现得像静态和私有的。
  • @Raynos:是的,它经常用于封装数据,定义对象的私有成员等。根据你对JavaScript的熟悉程度,我推荐这本书JavaScript Patterns
  • 我对第二个很熟悉,但是通过使用像这样的闭包来“内联”函数对我来说是新事物。
【解决方案2】:

最初的问题是在 2011 年提出的。鉴于此后 Node.js 的兴起,我认为值得重新审视这个问题。在服务器环境中,这里和那里的几毫秒可能很重要。在负载下是否保持响应可能有所不同。

虽然内部函数在概念上很好,但它们可能会给 JavaScript 引擎的代码优化器带来问题。下面的例子说明了这一点:

function a1(n) {
    return n + 2;
}

function a2(n) {
    return 2 - n;
}

function a() {
    var k = 5;
    for (var i = 0; i < 100000000; i++) {
        k = a1(k) + a2(k);
    }
    return k;
}

function b() {
    function b1(n) {
        return n + 2;
    }

    function b2(n) {
        return 2 - n;
    }

    var k = 5;
    for (var i = 0; i < 100000000; i++) {
        k = b1(k) + b2(k);
    }
    return k;
}

function measure(label, fn) {
    var s = new Date();
    var r = fn();
    var e = new Date();
    console.log(label, e - s);
}

for (var i = 0; i < 4; i++) {
    measure('A', a);
    measure('B', b);
}

运行代码的命令:

node --trace_deopt test.js

输出:

[deoptimize global object @ 0x2431b35106e9]
A 128
B 130
A 132
[deoptimizing (DEOPT eager): begin 0x3ee3d709a821 b (opt #5) @4, FP to SP delta: 72]
  translating b => node=36, height=32
    0x7fffb88a9960: [top + 64] <- 0x2431b3504121 ; rdi 0x2431b3504121 <undefined>
    0x7fffb88a9958: [top + 56] <- 0x17210dea8376 ; caller's pc
    0x7fffb88a9950: [top + 48] <- 0x7fffb88a9998 ; caller's fp
    0x7fffb88a9948: [top + 40] <- 0x3ee3d709a709; context
    0x7fffb88a9940: [top + 32] <- 0x3ee3d709a821; function
    0x7fffb88a9938: [top + 24] <- 0x3ee3d70efa71 ; rcx 0x3ee3d70efa71 <JS Function b1 (SharedFunctionInfo 0x361602434ae1)>
    0x7fffb88a9930: [top + 16] <- 0x3ee3d70efab9 ; rdx 0x3ee3d70efab9 <JS Function b2 (SharedFunctionInfo 0x361602434b71)>
    0x7fffb88a9928: [top + 8] <- 5 ; rbx (smi)
    0x7fffb88a9920: [top + 0] <- 0 ; rax (smi)
[deoptimizing (eager): end 0x3ee3d709a821 b @4 => node=36, pc=0x17210dec9129, state=NO_REGISTERS, alignment=no padding, took 0.203 ms]
[removing optimized code for: b]
B 1000
A 125
B 1032
A 132
B 1033

如您所见,函数 A 和 B 最初以相同的速度运行。然后由于某种原因发生了去优化事件。从那时起,B 几乎慢了一个数量级。

如果您正在编写注重性能的代码,最好避免使用内部函数。

【讨论】:

    【解决方案3】:

    这完全取决于函数被调用的频率。如果它是一个每秒调用 10 次的 OnUpdate 函数,那么它是一个不错的优化。如果每页调用 3 次,则属于微优化。

    虽然方便,但永远不需要嵌套函数定义(它们可以被函数的额外参数替换)。

    嵌套函数示例:

    function somefunc() {
        var localvar = 5
    
        var otherfunc = function() {
             alert(localvar);
        }
    
        otherfunc();
    }
    

    同样的事情,现在用参数代替:

    function otherfunc(localvar) {
        alert(localvar);
    }
    
    function somefunc() {
        var localvar = 5
    
        otherfunc(localvar);
    }
    

    【讨论】:

    • 您能解释一下如何避免嵌套函数吗?我主要使用它们来保持代码干燥,因为将它们放在该特定函数之外只会为全局命名空间添加更多内容,而它实际上应该是本地的。理想情况下,我想要一个静态函数,但这是不可能的。
    • 我想要本地函数的原因主要是在somefunc 中包含otherfunc,因为它只在那里使用并且内联它有点难看。
    【解决方案4】:

    绝对是微优化。首先拥有函数的全部原因是为了让你的代码更简洁、更易维护和更易读。函数为代码段添加语义边界。每个函数应该只做一件事,而且应该干净利落地做。因此,如果您发现自己的函数同时执行多项操作,则可以将其重构为多个例程。

    仅当您的工作速度太慢时才进行优化(如果还没有工作,那么优化还为时过早。期间)。请记住,没有人会为比他们的需求/要求更快的程序支付额外费用...

    编辑:考虑到程序还没有完成,这也是一个过早的优化。为什么这么糟糕?嗯,首先你要花时间做一些从长远来看可能无关紧要的事情。其次,您没有基准来查看您的优化是否在现实意义上改进了任何东西。第三,您甚至在运行之前就降低了可维护性和可读性,因此与使用干净简洁的代码相比,运行起来会更加困难。第四,在您完成并了解您的所有需求之前,您不知道您是否需要在程序中的其他地方使用doMoreStuff(可能取决于具体细节,但并非超出可能性范围)。

    Donnald Knuth 说 过早的优化是万恶之源是有原因的...

    【讨论】:

      【解决方案5】:

      在普通 PC 上运行的快速“基准测试”(我知道有很多无法解释的变量,所以不要评论显而易见的,但无论如何它都很有趣):

      count = 0;
      t1 = +new Date();
      while(count < 1000000) {
        p = function(){};
        ++count;
      }
      t2 = +new Date();
      console.log(t2-t1); // milliseconds
      

      例如,可以通过将增量移动到条件来优化它(使运行时间减少大约 100 毫秒,尽管它不会影响有和没有函数创建之间的差异,所以它不相关)

      跑了 3 次给了:

      913
      878
      890
      

      然后注释掉函数创建行,运行3次就给出了:

      462
      458
      464
      

      因此,仅在 1000,000 个空函数创建上,您添加大约半秒。即使假设您的原始代码在手持设备上每秒运行 10 次(假设设备的整体性能是这台笔记本电脑的 1/100,这是夸大的 - 它可能更接近 1/10,尽管会提供一个不错的上限) ,这相当于在这台计算机上每秒创建 1000 个函数,这发生在 1/2000 秒内。因此,手持设备每秒都会增加 1/2000 秒的处理开销……每秒半毫秒并不是很多。

      从这个原始测试中,我可以得出结论,在 PC 上,这绝对是一种微优化,如果您正在为较弱的设备进行开发,那几乎肯定也是如此。

      【讨论】:

      • 如果你将它与进行有意义计算的函数体或循环体进行比较,差异在于日期的统计误差边界。
      • @Raynos,我不知道你做了多少基准测试,尽管使用更大的循环体进行测试会破坏目的。如果你保持循环体最小,你可以更准确地测量函数创建的效果,从而最小化日期错误的影响。再加上您多次运行测试并获得准确测量的事实。我的观点是循环体无关紧要,你的问题本质上是,通过更改我的代码我可以节省多少,答案在于创建函数需要多少时间,最好按照我的描述来衡量。
      • 我想说的是,我认为这是一个稍微有意义的优化。我并没有真正节省多少。我一直认为这是不好的做法。
      【解决方案6】:

      为了使用嵌套函数(外部函数的内部范围内的函数)获得最佳速度,我怀疑您应该使用声明,而不是表达式。

      该问题询问“局部函数”和优化,但未指定如何创建局部函数。但它应该是,因为可以创建“内部功能”的不同技术的问题答案可能不同。

      查看@cleong 的答案和测试结果,我怀疑只有他的答案是使用最佳技术来创建函数。创建函数有三种方法,@cleong 向我们展示了一种提供快速执行的方法。这三种技术是:

      • 构造函数
      • 声明
      • 表达式

      构造函数用的不多,它需要一个包含函数体文本的字符串。这在反射式编程中很有用,您可以在其中执行“toString()”来获取函数体,修改,然后构造一个新函数。当然,这或多或少从未做过。

      声明 被使用,但主要用于外部函数,而不是内部函数(“内部函数”是指嵌套在另一个函数中的函数)。然而,基于@cleong 测试,它似乎非常快;和外部函数一样快。

      表达式是每个人都会使用的。这可能不是最好的主意;但这是每个人都会做的事情。

      函数声明和函数表达式之间的一个主要区别是声明受提升。每个人都知道“var”声明被提升了;但“函数”声明也是如此。对于被提升的事物,在编译时执行计算以确定事物所需的内存空间。大概,人们会期望内部函数在编译时被编译,并且可以像编译的外部函数一样运行。

      我有一本大约六年前的 Flannigan 的“权威指南”一书,我记得读过我刚刚在这里写的相反的内容。他说过类似的话:表达式是编译的,而声明不是。虽然他是 JavaScript 的世界“权威指南”,但我一直怀疑他可能把这本书搞混了。我怀疑函数内部声明比函数表达式更“准备就绪”。这个 stackOverflow 页面上的测试结果似乎证实了我长期以来的怀疑。

      查看@cleong 测试结果,如果考虑最佳执行速度,似乎声明而不是表达式是内部函数的方法。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2013-06-25
        • 2016-01-13
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2019-01-13
        • 1970-01-01
        相关资源
        最近更新 更多