谷歌闭包编译器是否会降低性能?

30

我正在编写一个Google Chrome扩展程序。由于JavaScript文件是从磁盘加载的,因此它们的大小几乎不重要。

尽管如此,我仍然使用了Google Closure Compiler,因为它可以进行性能优化以及减少代码大小。

但我注意到在Closure Compiler的输出顶部有这样一段内容:

var i = true, m = null, r = false;
这样做显然是为了减少文件大小(脚本中随后使用的所有true/null/false都可以用单个字符替换)。
但是这样做是否会稍微影响性能呢?直接读取字面量true关键字肯定比查找变量名并找到它的值是true要快吧...?
这种性能影响值得担心吗?Google Closure Compiler还有其他可能会减慢执行速度的功能吗?

1
我怀疑性能损失不大,但文件大小的减少肯定会超过它。 - Rafe Kettler
我相信这个问题的真正答案在于v8或其他js JIT是否进行全局常量传播和折叠(我相信它们在某种程度上会这样做)。 - Necrolis
21
使用“GCC”作为“Google Closure Compiler”的缩写很可能会导致混淆。http://gcc.gnu.org/ - Mike Samuel
3
这个问题已经不再实际存在,因为编译器现在使用!0代表true,使用!1代表false - Rok Kralj
1
@RokKralj,仍然有效。当然,!0true慢。 - Pacerier
3个回答

43

答案可能是maybe

让我们看看闭包团队对此的说法。

来自FAQ:

编译器在应用程序的执行速度和下载代码大小之间做出了任何权衡吗?

是的。任何优化编译器都会进行权衡。 某些大小优化确实会增加一些小的速度开销。但是,Closure编译器的开发人员已经小心翼翼地避免引入显著的额外运行时间。编译器的一些优化甚至可以减少运行时间(见下一个问题)。

编译器是否针对速度进行优化?

在大多数情况下,较小的代码更快,因为下载时间通常是Web应用程序中最重要的速度因素。减少冗余的优化也会加速代码的运行时间。

我断然挑战他们在这里提出的第一个假设。使用变量名称的大小并不直接影响各种JavaScript引擎处理代码的方式 - 实际上,JS引擎不关心您是否将变量称为supercalifragilisticexpialidocious还是x(但作为程序员的我确实关心)。如果你担心交付,下载时间是最重要的部分 - 慢运行脚本可能由数百万个我怀疑工具无法解决的问题引起。

要真正理解您的问题为什么可能是“maybe”,首先需要问的问题是“是什么使JavaScript快还是慢?”

然后当然我们会遇到一个问题,“我们讨论的是哪个JavaScript引擎?”

我们有:

  • Carakan(Opera)
  • Chakra(IE9+)
  • SpiderMonkey(Mozilla / FireFox)
  • SquirrelFish(Apple的Webkit)
  • V8(Chrome)
  • Futhark(Opera)
  • JScript(IE 9之前的所有版本)
  • JavaScriptCore(Konqueror,Safari)
  • 我省略了一些.

这里有人真的认为它们都是一样的吗?特别是JScript和V8?绝不!

那么,当Google Closure编译代码时,它是在为哪个引擎构建东西呢?你感觉幸运吗?

好的,因为我们永远不可能涵盖所有这些基础,让我们试着更一般地看看“旧”与“新”代码。

以下是从我见过的最好的JS引擎介绍之一摘录的有关此特定部分的快速摘要。

较旧的JS引擎

  • 代码被解释并直接编译为字节码
  • 没有优化:你得到什么就是什么
    var FunctionForIntegersOnly = function(int1, int2){
        return int1 + int2;
    }
    
    var FunctionForStringsOnly = function(str1, str2){
        return str1 + str2;
    }
    
    alert(FunctionForIntegersOnly(1, 2) + FunctionForStringsOnly("a", "b"));
    

    将其通过Google Closure运行,实际上可以将整个代码简化为:

    alert("3ab");
    

    而且按照书上的每个指标,它都快得多。这里真正发生的是,它简化了我的非常简单的例子,因为它执行了一部分操作。然而,在这种情况下,你需要小心。

    假设我们的代码中有一个Y组合子,编译器将其转换为类似于以下内容的东西:

    (function(a) {
     return function(b) {
        return a(a)(b)
      }
    })(function(a) {
      return function(b) {
        if(b > 0) {
          return console.log(b), a(a)(b - 1)
        }
      }
    })(5);
    

    并没有真正提高速度,只是缩小了代码。

    JIT通常会看到你的代码只接受两个字符串输入并返回一个字符串(或者对于第一个函数,返回一个整数),然后将其放入特定类型的JIT中,使其变得非常快。现在,如果Google Closure做一些奇怪的事情,比如将那些具有几乎相同签名的函数转换为一个函数(对于非平凡代码),如果编译器做了JIT不喜欢的事情,您可能会失去JIT速度。

    那么,我们学到了什么?

    • 你可能拥有经过JIT优化的代码,但编译器会重新组织你的代码成为其他形式
    • 旧版浏览器没有JIT但仍会运行你的代码
    • Closure编译的JS通过对简单函数进行部分执行来调用更少的函数调用。

    那么你该怎么办?

    • 编写简短明了的函数,编译器将能够更好地处理它们
    • 如果你深刻理解JIT、手动优化代码,并且使用了这些知识,那么使用Closure编译器可能没有价值。
    • 如果你希望代码在旧版浏览器上运行得更快,那么这是一个绝佳的工具
    • 权衡通常是值得的,但一定要小心检查事情,不要盲目信任。

    总的来说,你的代码会更快。如果你的代码使用较小的函数和正确的原型面向对象设计,可能会引入一些各种JIT编译器不喜欢的东西,但它们很少见。如果你考虑编译器正在做的全部范围(下载时间更短且执行更快),那么奇怪的事情,比如var i = true, m = null, r = false; 可能是编译器做出的值得权衡的决策,即使它们运行地较慢,总体寿命也会更快。

    值得注意的是,Web应用程序执行中最常见的瓶颈是文档对象模型,如果你的代码运行缓慢,我建议你将更多的精力放在那里。


3
非常好的回答,但我担心如果“常量”值在变量中,JIT编译器可能会更难进行内联和部分评估,因为这个变量有可能稍后被重新赋值。 - andrewmu
3
@incognito说:“我直接挑战他们在这里做出的第一个假设。变量名的大小...” 我们并没有做出那个假设。你从哪里得到Closure Compiler把变量名大小和运行时性能等同起来的想法?不过,应用程序的启动时间与下载代码所需的时间高度相关,每个字节都很重要(可悲的是)。由于移动平台具有高延迟/低带宽/低可靠连接、次优处理器和微小的浏览器缓存,这一事实在过去十年中变得更加严重。 - John
2
我应该澄清一下,除了奇怪的开头外,总体评估是正确的,但我想补充说,由于至少有4个主要引擎(每个主要浏览器都有一个引擎,而不考虑即将推出的引擎),每个引擎都具有不同的特点,因此“调整JIT”是困难的。 - John
@Incognito,你倒数第二段提到“总寿命更快”,这里的“总寿命”是指什么? - Pacerier
@John,谷歌很可能正在调整自己的JIT(而不是一般的JIT),以便Chrome赢得竞争对手。 - Pacerier
显示剩余2条评论

10

在现代浏览器中,使用字面量truenull与使用变量在几乎所有情况下(即零)没有任何区别(它们完全相同)。 在极少数情况下,变量实际上更快。

因此,在节省这些额外的字节方面不值得花费任何成本。

true与变量比较 (http://jsperf.com/true-vs-variable):

true vs variable

null与变量比较 (http://jsperf.com/null-vs-variable):

null vs variable


在极少数情况下,变量实际上更快。请解释一下,这是怎么回事? - Pacerier
@Pacerier 我不知道。那只是数据(可在jsperf上获得)。 - ThinkingStiff
3
那就证明我们所测量的并不是我们想要测量的东西。某个地方出现了问题,这些时间数据不能被信任。 - Pacerier

2
我认为会有一定的性能损失,但在较新的现代浏览器中可能不会影响太多。
请注意,Closure Compiler的标准别名变量都是全局变量。这意味着,在旧的浏览器中,JavaScript引擎需要花费线性时间来遍历函数作用域(例如IE <9),你所在的嵌套函数调用层数越深,则查找保存“true”或“false”等值的变量就需要更长的时间。几乎所有现代JavaScript引擎都优化了全局变量的访问,因此在许多情况下,这种性能损失应该不再存在。
此外,在编译后的代码中,除了赋值或参数之外,真正出现“true”、“false”或“null”的地方应该不会很多。例如:if (someFlag == true) ... 大多数情况下只需写成 if (someFlag) ...,编译器将其编译成 a && ...。你主要只会在赋值 (someFlag = true;) 和参数 (someFunc(true);) 中看到它们,而这些情况并不经常发生。
结论是:尽管许多人怀疑Closure Compiler的标准别名的实用性(我也是其中之一),但你不应该期望有任何实质性的性能损失。但是,在gzip压缩后的大小方面,你也不应该期望有任何实质性的好处。

请注意:"if (someFlag === true) ..." 不会被折叠成 "a && ..."。通常,Closure 编译器在不知道变量可能具有的值的情况下进行这种重写,而且只有当其唯一可能的值是 "true" 和所谓的 falsy 值(false、null、undefined 等)时,"a === true" 才能简化为 "a"。 - John
@John,你说得对。if (someFlag) ...被折叠成a && ...,但=== true== true被折叠成a === true && ...。我会编辑我的答案。谢谢! - Stephen Chung

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接