为什么Closure Compiler坚持添加更多字节?

20

如果我给Closure Compiler这样的东西:

window.array = '0123456789'.split('');

它将其“编译”为以下内容:

window.array="0,1,2,3,4,5,6,7,8,9".split(",");

现在你可以看出来,它更大了。闭包编译器为什么会这样做呢?


1
没有任何理由,据我所知,但在缩小方面没有完美的东西... - Ry-
7
可能是因为它更快(至少在Chrome中,即使速度提升不多):http://jsperf.com/empty-split-vs-comma-split 有趣的是看看其他浏览器的差异... 编辑: 在Safari和Firefox中,看起来速度较慢...那我就不知道了。但总的来说,在边缘情况下,CC更注重速度而非大小。 - Felix Kling
也许是为了兼容性? - SLaks
@Matt:这就是我说“可能”的原因。但这种情况可能会导致IE6出现问题。 - SLaks
我的最初想法与@SLaks相同,但我几乎可以确定它在任何地方都能工作。我已经在IE6中进行了测试,没有发现任何问题。 - James Allardice
2
刚测试了一下 - 在我的IE4中无法工作。 - Florian Margaine
3个回答

21

我认为这就是发生的事情,但我绝不确定...

导致插入逗号的代码位于PeepholeSubstituteAlternateSyntax.java中的tryMinimizeStringArrayLiteral方法。

该方法包含一个字符列表,这些字符可能具有较低的哈夫曼编码,因此与其他字符相比,它们更适合拆分。如果您尝试类似于下面的内容,则可以看到其结果:

"a b c d e f g".split(" "); //Uncompiled, split on spaces
"a,b,c,d,e,f,g".split(","); //Compiled, split on commas (same size)
编译器会用它认为最有利的字符替换您尝试拆分的字符。 它通过遍历字符串的字符并找到不在字符串中出现的最有利的拆分字符来完成此操作:
// These delimiters are chars that appears a lot in the program therefore
// probably have a small Huffman encoding.
NEXT_DELIMITER: for (char delimiter : new char[]{',', ' ', ';', '{', '}'}) {
  for (String cur : strings) {
    if (cur.indexOf(delimiter) != -1) {
      continue NEXT_DELIMITER;
    }
  }
  String template = Joiner.on(delimiter).join(strings);
  //...
}
在上面的代码片段中,您可以看到编译器认为最佳分隔符的字符数组。逗号在第一位(这就是为什么在我的空格示例中,空格被替换为逗号的原因)。
我认为在要分割的字符串为空字符串的情况下插入逗号可能只是疏忽。似乎没有对这种情况进行任何特殊处理,因此它像任何其他split调用一样处理,并且每个字符都与上述代码片段中显示的第一个适当的字符连接起来。
编译器如何处理split方法的另一个示例:
"a,;b;c;d;e;f;g".split(";"); //Uncompiled, split on semi-colons
"a, b c d e f g".split(" "); //Compiled, split on spaces

由于原始字符串已经包含了逗号(我们不想在逗号字符上进行拆分),因此逗号不能从低Huffman编码字符数组中选择,所以选择了次优选择(空格)。


更新

经过进一步的研究,这绝对不是一个错误。实际上,这种行为是有意设计的,而且在我看来,这是一个非常聪明的小优化,考虑到Closure编译器倾向于编译代码的速度而不是大小。

上面我提到了霍夫曼编码几次。霍夫曼编码算法简单地解释就是给文本中出现的每个字符分配一个权值。该权值基于每个字符出现的频率。这些频率用于构建二叉树,且最常见的字符位于根部。这意味着最常见的字符更容易解码,因为它们靠近树的根部。

由于Huffman算法是gzip使用的DEFLATE算法的重要组成部分。因此,如果您的Web服务器配置为使用gzip,则您的用户将从这个巧妙的优化中受益。


说到这个,拆分赋值和常规赋值已经非常慢了:jsperf - Oleg V. Volkov
@OlegV.Volkov - 是的,如果将调用split转换为直接赋值可以使代码更短,那么闭包编译器会进行转换。 - James Allardice
@FelixKling - 谢谢!你(或其他任何人)能否想到任何会导致.split("")失败或以意外方式工作的情况?我认为没有这样的情况(我在许多浏览器中进行了测试,没有发现任何问题),因此值得提交错误报告。它似乎是一个相当大的疏忽,所以我很惊讶它以前没有出现过,这让我觉得可能是有意设计的。 - James Allardice
好的,这个问题以前出现过(我很惊讶之前没有发现这个问题)-请参见thg435在他们的回答评论中发布的链接。这不值得提交错误报告。 - James Allardice
@James Allardice,你对编译器的评价有点高了。它将常量分割转换为数组,希望能将数组转换为更简单的形式。这是为了支持“在高级模式下删除未使用的jQuery部分”的努力而添加的。jQuery在某些地方使用此模式来定义方法。 qwertymk如果这对你很重要,那么这将是一个相当简单的补丁。 - John
显示剩余2条评论

5

你应该检查一下这个URL。 - Denys Séguret
更新了URL并添加了修订日期,因为GIT修订ID对于排序是无用的。 - John

4

具有讽刺意味的是,编译后的代码中的split与源代码中的split没有任何关系。请考虑以下内容:

Source  : a = ["0","1","2","3","4","5"]
Compiled: a="0,1,2,3,4,5".split(",")

这里的split只是一种表示长数组的方法(足够长,使得所有引号和逗号的总和比split(","")还要长)。那么,在你的例子中发生了什么?首先,编译器看到一个应用于常量的字符串函数,并立即评估它:
'0123456789'.split('') => ["0","1","2","3","4","5","6","7","8","9"]

在稍后的某个时刻,当生成输出时,编译器会将这个数组视为“长”的,并按照上述的“分割”形式进行编写:

["0","1","2","3","4","5","6","7","8","9"] => "0,1,2,3,4,5,6,7,8,9".split(",")

请注意,此时源代码中关于split('')的所有信息都已丢失。
如果源字符串较短,则将以数组形式生成数组形式,无需额外拆分:
Source  : a = '0123'.split('')
Compiled: a=["0","1","2","3"]

那么['0', '1', '2', '3', '4', '5', '6', '7']不应该总是编译为'01234567'.split('')吗? - qwertymk
@qwertymk - 这正是它所做的,不是吗?只要所需字符数少于split调用,它就会编译任何直接字符串数组赋值。查看我回答中提到的方法的实现,以了解确切的情况。 - James Allardice
@JamesAllardice:我是指没有空格和空分隔符。 - qwertymk
@qwertymk - 哦,对不起,我误读了你的评论。是的,那可能就是它应该编译成的样子。我认为你可能发现了Closure编译器团队犯下的一个很大的疏忽! - James Allardice
3
是的,他们可以检查数组中的所有字符串是否都是长度为1的,并提供此优化功能,但是引用其中一位维护者的话,“单个字母的示例似乎是一个特殊情况,我必须质疑是否值得努力去解决”(http://code.google.com/p/closure-compiler/issues/detail?id=702) - georg
@thg435 - 不错的发现。我看了那些问题,但从没注意到那一个。 - James Allardice

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