为什么后者函数必须一遍又一遍地创建变量,但为什么却快10%?


14
var toSizeString = (function() {

 var KB = 1024.0,
     MB = 1024 * KB,
     GB = 1024 * MB;

  return function(size) {
    var gbSize = size / GB,
        gbMod  = size % GB,
        mbSize = gbMod / MB,
        mbMod  = gbMod % MB,
        kbSize = mbMod / KB;

    if (Math.floor(gbSize)) {
      return gbSize.toFixed(1) + 'GB';
    } else if (Math.floor(mbSize)) {
      return mbSize.toFixed(1) + 'MB';
    } else if (Math.floor(kbSize)) {
      return kbSize.toFixed(1) + 'KB';
    } else {
      return size + 'B';
    }
  };
})();

还有更快的功能:(请注意,它必须始终一遍又一遍地计算相同的变量kb / mb / gb)。它在哪里获得性能?

function toSizeString (size) {

 var KB = 1024.0,
     MB = 1024 * KB,
     GB = 1024 * MB;

 var gbSize = size / GB,
     gbMod  = size % GB,
     mbSize = gbMod / MB,
     mbMod  = gbMod % MB,
     kbSize = mbMod / KB;

 if (Math.floor(gbSize)) {
      return gbSize.toFixed(1) + 'GB';
 } else if (Math.floor(mbSize)) {
      return mbSize.toFixed(1) + 'MB';
 } else if (Math.floor(kbSize)) {
      return kbSize.toFixed(1) + 'KB';
 } else {
      return size + 'B';
 }
};

3
在任何静态类型语言中,“变量”都将被编译为常量。也许现代的JS引擎能够执行相同的优化。如果变量是闭包的一部分,这似乎不起作用。
usr

6
这是您使用的JavaScript引擎的实现细节。理论上的时间和空间是相同的,只是给定JavaScript引擎的实现会改变它们。因此,为了正确回答您的问题,您需要列出用来衡量这些问题的特定JavaScript引擎。也许有人知道它的实现细节,然后说出它如何/为什么比另一个更优化。另外,您应该发布您的测量代码。
吉米·霍法

您将“计算”一词用于表示常量值;您所引用的内容实际上没有什么可计算的。常量值的算术运算是编译器最简单,最明显的优化方法之一,因此,只要您看到仅包含常量值的表达式,就可以假定整个表达式都被优化为单个常量值。
Jimmy Hoffa 2015年

@JimmyHoffa没错,但是另一方面,每个函数调用都需要创建3个常量变量...
Tomy

@Tomy常量不是变量。它们没有变化,因此无需在编译后重新创建。通常将一个常量放置在内存中,并且该常量的每个将来范围都将定向到完全相同的位置,无需重新创建它,因为它的值永远不会改变,因此它不是变量。编译器通常不会发出用于创建常量的代码,而是由编译器进行创建,并将所有代码引用定向到其生成的内容。
吉米·霍法

Answers:


23

现代的JavaScript引擎都可以即时编译。您不能对其“必须一遍又一遍地创建”进行任何假设。无论哪种情况,这种计算都相对容易优化。

另一方面,关闭常量变量并不是针对JIT编译的典型情况。当您希望能够在不同的调用中更改这些变量时,通常会创建一个闭包。您还将创建一个额外的指针取消引用来访问这些变量,例如访问成员变量和OOP中的本地int之间的区别。

这种情况就是为什么人们抛弃“过早优化”的原因。简单的优化已经由编译器完成。


我怀疑是因为变量分辨率的范围遍历导致了您所提到的损失。看起来很合理,但是谁真正知道JavaScript JIT引擎中有什么疯狂……
Jimmy Hoffa 2015年

1
这个答案的可能扩展:JIT会忽略对于脱机编译器来说很容易的优化的原因是因为整个编译器的性能比异常情况更重要。
Leushenko

12

变量很便宜。执行上下文和作用域链很昂贵。

从本质上讲,有多种答案可以归结为“因为闭包”,而这些答案基本上是正确的,但是问题并不是专门针对闭包,这是因为您有一个函数引用了不同范围内的变量。如果这些是window对象上的全局变量,而不是IIFE中的局部变量,那么您将遇到同样的问题。试试看。

因此,在第一个函数中,当引擎看到以下语句时:

var gbSize = size / GB;

它必须采取以下步骤:

  1. size在当前范围内搜索变量。(找到了。)
  2. GB在当前范围内搜索变量。(未找到。)
  3. GB在父范围中搜索变量。(找到了。)
  4. 进行计算并分配给gbSize

步骤3比分配变量要昂贵得多。此外,这样做五次,其中两次都GBMB。我怀疑如果您在函数的开头(例如var gb = GB)对它们进行了别名并引用了别名,则实际上会产生很小的提速,尽管某些JS引擎也可能已经执行了此优化。当然,加速执行的最有效方法就是根本不遍历作用域链。

请记住,JavaScript不同于编译器在编译时解析这些变量地址的静态类型语言。JS引擎必须按名称解析它们,并且这些查找每次都在运行时发生。因此,您希望尽可能避免使用它们。

变量赋值在JavaScript中非常便宜。实际上,这可能是最便宜的操作,尽管我没有什么可以支持该声明的。但是,可以肯定地说,避免创建变量几乎从来不是一个好主意。实际上,您尝试在该领域中进行的所有优化最终都会使性能恶化。


而且,即使在“优化”不产生负面影响的表现,几乎可以肯定会产生负面影响代码的可读性。除非您正在做一些疯狂的计算工作,否则通常这是一个不好的权衡(很不幸,显然没有固定链接锚;搜索“ 2009-02-17 11:41”)。总结如下:“如果不是绝对需要速度,则选择清晰度而不是速度。”
CVn

即使为动态语言编写非常基本的解释器,运行时期间的变量访问也倾向于是O(1)操作,并且在初始编译期间甚至不需要O(n)范围遍历。在每个作用域中,每个新声明的变量都分配有一个数字,因此给定的var a, b, c我们可以访问bas scope[1]。所有作用域均已编号,并且如果此作用域嵌套了五个作用域,则在解析过程中b可以完全解决env[5][1]该问题。在本机代码中,范围对应于堆栈段。闭包是一个更复杂,因为它们必须备份和更换env
天宫院

@amon:这可能是你怎么会非常喜欢它的工作,但它不是它如何工作。人们比我写过的书知识和经验要丰富得多。特别要指出的是Nicholas C. Zakas的High Performance JavaScript这是一个代码片段,他还与基准测试进行了对话以支持它。当然,他当然不是唯一的,而是最有名的。JavaScript具有词法作用域,因此闭包实际上并不是那么特别-本质上,所有内容都是闭包。
亚伦诺特,2015年

@Aaronaught有趣。由于该书已有5年历史,所以我对当前的JS引擎如何处理变量查找以及V8引擎的x64后端很感兴趣。在静态分析期间,大多数变量都是静态解析的,并在其作用域内分配了内存偏移量。函数范围表示为链接列表,而汇编程序作为展开的循环发出以达到正确的范围。在这里,我们将获得与C代码等效*(scope->outer + variable_offset)的访问权限;每个额外的功能范围级别都会花费一个额外的指针取消引用。似乎我们都是对的:)
阿蒙(Amon)2015年

2

一个示例涉及闭包,而另一个则不涉及。实现闭包有点棘手,因为对变量的闭包不能像普通变量那样工作。在像C这样的低级语言中,这一点更加明显,但是我将使用JavaScript进行说明。

闭包不仅包含一个函数,而且还包含它所封闭的所有变量。当我们想调用该函数时,我们还需要提供所有封闭的变量。我们可以通过一个函数来对闭包进行建模,该函数接收一个对象作为第一个参数,该对象表示这些封闭变量:

function add(vars, y) {
  vars.x += y;
}

function getSum(vars) {
  return vars.x;
}

function makeAdder(x) {
  return { x: x, add: add, getSum: getSum };
}

var adder = makeAdder(40);
adder.add(adder, 2);
console.log(adder.getSum(adder));  //=> 42

请注意closure.apply(closure, ...realArgs)这需要的尴尬的调用约定

JavaScript的内置对象支持使省略显式vars参数成为可能,并让我们this改为使用:

function add(y) {
  this.x += y;
}

function getSum() {
  return this.x;
}

function makeAdder(x) {
  return { x: x, add: add, getSum: getSum };
}

var adder = makeAdder(40);
adder.add(2);
console.log(adder.getSum());  //=> 42

这些示例实际上等于使用闭包的以下代码:

function makeAdder(x) {
  return {
    add: function (y) { x += y },
    getSum: function () { return x },
  };
}

var adder = makeAdder(40);
adder.add(2);
console.log(adder.getSum());  //=> 42

在最后一个示例中,该对象仅用于对两个返回的函数进行分组。该this结合是无关紧要的。语言会处理所有使闭包成为可能的细节-将隐藏数据传递给实际函数,将对闭包变量的所有访问权限更改为该隐藏数据中的查找-均由语言负责。

但是,调用闭包会带来传入额外数据的开销,而运行闭包会带来在该额外数据中查找的开销–由于缓存位置不佳以及与普通变量相比,指针的取消引用而使情况更加糟糕–因此,不依赖闭包的解决方案效果更好。特别是由于闭包为您节省的所有工作都是一些非常便宜的算术运算,这些运算甚至可能在解析过程中不断折叠。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.