炼数成金 门户 大数据 JavaScript 查看内容

探索传统 JavaScript 基准测试(上)

2017-10-5 18:44| 发布者: 炼数成金_小数| 查看: 35925| 评论: 0|来自: Linux中国

摘要: 可以很公平地说,JavaScript 是当下软件工程中最重要的技术。对于那些深入接触过编程语言、编译器和虚拟机的人来说,这仍然有点令人惊讶,因为在语言设计者们看来,JavaScript 不是十分优雅;在编译器工程师们看来, ...

Java 测试 案例 JavaScript 性能测试

可以很公平地说,JavaScript[1] 是当下软件工程中最重要的技术。对于那些深入接触过编程语言、编译器和虚拟机的人来说,这仍然有点令人惊讶,因为在语言设计者们看来,JavaScript 不是十分优雅;在编译器工程师们看来,它没有多少可优化的地方;甚至还没有一个伟大的标准库。这取决于你和谁吐槽,JavaScript 的缺点你花上数周都枚举不完,而你总会找到一些你从所未知的奇怪的东西。尽管这看起来明显困难重重,不过 JavaScript 还是成为了当今 web 的核心,并且还(通过 Node.js[2])成为服务器端和云端的主导技术,甚至还开辟了进军物联网领域的道路。

那么问题来了,为什么 JavaScript 如此受欢迎?或者说如此成功?我知道没有一个很好的答案。如今我们有许多使用 JavaScript 的好理由,或许最重要的是围绕其构建的庞大的生态系统,以及现今大量可用的资源。但所有这一切实际上是发展到一定程度的后果。为什么 JavaScript 变得流行起来了?嗯,你或许会说,这是 web 多年来的通用语了。但是在很长一段时间里,人们极其讨厌 JavaScript。回顾过去,似乎第一波 JavaScript 浪潮爆发在上个年代的后半段。那个时候 JavaScript 引擎加速了各种不同的任务的执行,很自然的,这可能让很多人对 JavaScript 刮目相看。

回到过去那些日子,这些加速使用了现在所谓的传统 JavaScript 基准进行测试——从苹果的 SunSpider 基准[3](JavaScript 微基准之母)到 Mozilla 的 Kraken 基准[4] 和谷歌的 V8 基准。后来,V8 基准被 Octane 基准[5] 取代,而苹果发布了新的 JetStream 基准[6]。这些传统的 JavaScript 基准测试驱动了无数人的努力,使 JavaScript 的性能达到了本世纪初没人能预料到的水平。据报道其性能加速达到了 1000 倍,一夜之间在网站使用 <script> 标签不再是与魔鬼共舞,做客户端不再仅仅是可能的了,甚至是被鼓励的。

性能测试,JS 基准的简史
(来源: Advanced JS performance with V8 and Web Assembly[7], Chrome Developer Summit 2016, @s3ththompson。)

现在是 2016 年,所有(相关的)JavaScript 引擎的性能都达到了一个令人难以置信的水平,web 应用像原生应用一样快(或者能够像原生应用一样快)。引擎配有复杂的优化编译器,通过收集之前的关于类型/形状的反馈来推测某些操作(例如属性访问、二进制操作、比较、调用等),生成高度优化的机器代码的短序列。大多数优化是由 SunSpider 或 Kraken 等微基准以及 Octane 和 JetStream 等静态测试套件驱动的。由于有像 asm.js[8] 和 Emscripten[9] 这样的 JavaScript 技术,我们甚至可以将大型 C++ 应用程序编译成 JavaScript,并在你的浏览器上运行,而无需下载或安装任何东西。例如,现在你可以在 web 上玩 AngryBots[10],无需沙盒,而过去的 web 游戏需要安装一堆诸如 Adobe Flash 或 Chrome PNaCl 的特殊插件。

这些成就绝大多数都要归功于这些微基准和静态性能测试套件的出现,以及与这些传统的 JavaScript 基准间的竞争的结果。你可以对 SunSpider 表示不满,但很显然,没有 SunSpider,JavaScript 的性能可能达不到今天的高度。好吧,赞美到此为止。现在看看另一方面,所有的静态性能测试——无论是微基准micro-benchmark还是大型应用的宏基准macro-benchmark,都注定要随着时间的推移变成噩梦!为什么?因为在开始摆弄它之前,基准只能教你这么多。一旦达到某个阔值以上(或以下),那么有益于特定基准的优化的一般适用性将呈指数级下降。例如,我们将 Octane 作为现实世界中 web 应用性能的代表,并且在相当长的一段时间里,它可能做得很不错,但是现在,Octane 与现实场景中的时间分布是截然不同的,因此即使眼下再优化 Octane 乃至超越自身,可能在现实世界中还是得不到任何显著的改进(无论是通用 web 还是 Node.js 的工作负载)。

基准与现实世界的时间分布对比
(来源:Real-World JavaScript Performance[11],BlinkOn 6 conference,@tverwaes)

由于传统 JavaScript 基准(包括版的 JetStream 和 Octane)可能已经背离其有用性变得越来越远,我们开始在 2016 年初寻找新的方法来测量现实场景的性能,为 V8 和 Chrome 添加了大量新的性能追踪钩子。我们还特意添加一些机制来查看我们在浏览 web 时的时间究竟开销在哪里,例如,是脚本执行、垃圾回收、编译,还是什么地方?而这些调查的结果非常有趣和令人惊讶。从上面的幻灯片可以看出,运行 Octane 花费了 70% 以上的时间去执行 JavaScript 和垃圾回收,而浏览 web 的时候,通常执行 JavaScript 花费的时间不到 30%,垃圾回收占用的时间永远不会超过 5%。在 Octane 中并没有体现出它花费了大量时间来解析和编译。因此,将更多的时间用在优化 JavaScript 执行上将提高你的 Octane 跑分,但不会对加载 youtube.com[12] 有任何积极的影响。事实上,花费更多的时间来优化 JavaScript 执行甚至可能有损你现实场景的性能,因为编译器需要更多的时间,或者你需要跟踪更多的反馈,最终在编译、垃圾回收和运行时桶Runtime bucket等方面开销了更多的时间。


测速表
还有另外一组基准测试用于测量浏览器整体性能(包括 JavaScript 和 DOM 性能),推出的是 Speedometer 基准[13]。该基准试图通过运行一个用不同的主流 web 框架实现的简单的 TodoMVC[14] 应用(现在看来有点过时了,不过新版本正在研发中)以捕获更真实的现实场景的性能。上述幻灯片中的各种测试 (Angular、Ember、React、Vanilla、Flight 和 Backbone)挨着放在 Octane 之后,你可以看到,此时此刻这些测试似乎更好地代表了现实世界的性能指标。但是请注意,这些数据收集在本文撰写将近 6 个月以前,而且我们优化了更多的现实场景模式(例如我们正在重构垃圾回收系统以显著地降低开销,并且 解析器也正在重新设计[15])。还要注意的是,虽然这看起来像是只和浏览器相关,但我们有非常强有力的证据表明传统的峰值性能基准也不能很好的代表现实场景中 Node.js 应用性能。


Speedometer 和 Octane 对比
(来源: Real-World JavaScript Performance[11], BlinkOn 6 conference, @tverwaes.)

所有这一切可能已经路人皆知了,因此我将用本文剩下的部分强调一些具体案例,它们对关于我为什么认为这不仅有用,而且必须停止关注某一阈值的静态峰值性能基准测试对于 JavaScript 社区的健康是很关键的。让我通过一些例子说明 JavaScript 引擎怎样来玩弄基准的。

臭名昭著的 SunSpider 案例
一篇关于传统 JavaScript 基准测试的博客如果没有指出 SunSpider 那个明显的问题是不完整的。让我们从性能测试的较佳实践开始,它在现实场景中不是很适用:bitops-bitwise-and.js 性能测试[16]。

bitops-bitwise-and.js
有一些算法需要进行快速的 AND 位运算,特别是从 C/C++ 转译成 JavaScript 的地方,所以快速执行该操作确实有点意义。然而,现实场景中的网页可能不关心引擎在循环中执行 AND 位运算是否比另一个引擎快两倍。但是再盯着这段代码几秒钟后,你可能会注意到在第一次循环迭代之后 bitwiseAndValue 将变成 0,并且在接下来的 599999 次迭代中将保持为 0。所以一旦你让此获得了好的性能,比如在差不多的硬件上所有测试均低于 5ms,在经过尝试之后你会意识到,只有循环的第一次是必要的,而剩余的迭代只是在浪费时间(例如 loop peeling[17] 后面的死代码),那你现在就可以开始玩弄这个基准测试了。这需要 JavaScript 中的一些机制来执行这种转换,即你需要检查 bitwiseAndValue 是全局对象的常规属性还是在执行脚本之前不存在,全局对象或者它的原型上必须没有拦截器。但如果你真的想要赢得这个基准测试,并且你愿意全力以赴,那么你可以在不到 1ms 的时间内完成这个测试。然而,这种优化将局限于这种特殊情况,并且测试的轻微修改可能不再触发它。

好吧,那么 bitops-bitwise-and.js[16] 测试彻底肯定是微基准最失败的案例。让我们继续转移到 SunSpider 中更逼真的场景——string-tagcloud.js[18] 测试,它基本上是运行一个较早版本的 json.js polyfill。该测试可以说看起来比位运算测试更合理,但是花点时间查看基准的配置之后立刻会发现:大量的时间浪费在一条 eval 表达式(高达 20% 的总执行时间被用于解析和编译,再加上实际执行编译后代码的 10% 的时间)。

string-tagcloud.js
仔细看看,这个 eval 只执行了一次,并传递一个 JSON 格式的字符串,它包含一个由 2501 个含有 tag 和 popularity 属性的对象组成的数组:

([
  {
    "tag": "titillation",
    "popularity": 4294967296
  },
  {
    "tag": "foamless",
    "popularity": 1257718401
  },
  {
    "tag": "snarler",
    "popularity": 613166183
  },
  {
    "tag": "multangularness",
    "popularity": 368304452任何
  },
  {
    "tag": "Fesapo unventurous",
    "popularity": 248026512
  },
  {
    "tag": "esthesioblast",
    "popularity": 179556755
  },
  {
    "tag": "echeneidoid",
    "popularity": 136641578
  },
  {
    "tag": "embryoctony",
    "popularity": 107852576
  },
  ...
])
显然,解析这些对象字面量,为其生成本地代码,然后执行该代码的成本很高。将输入的字符串解析为 JSON 并生成适当的对象图的开销将更加低廉。所以,加快这个基准测试的一个小把戏就是模拟 eval,并尝试总是将数据首先作为 JSON 解析,如果以 JSON 方式读取失败,才回退进行真实的解析、编译、执行(尽管需要一些额外的黑魔法来跳过括号)。早在 2007 年,这甚至不算是一个坏点子,因为没有 JSON.parse[19],不过在 2017 年这只是 JavaScript 引擎的技术债,可能会让 eval 的合法使用遥遥无期。

--- string-tagcloud.js.ORIG     2016-12-14 09:00:52.869887104 +0100
+++ string-tagcloud.js  2016-12-14 09:01:01.033944051 +0100
@@ -198,7 +198,7 @@
                     replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(:?[eE][+\-]?\d+)?/g, ']').
                     replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) {
-                j = eval('(' + this + ')');
+                j = JSON.parse(this);
                 return typeof filter === 'function' ? walk('', j) : j;
             }
事实上,将基准测试更新到现代 JavaScript 会立刻会性能暴增,正如今天的 V8 LKGR 从 36ms 降到了 26ms,性能足足提升了 30%!

$ node string-tagcloud.js.ORIG
Time (string-tagcloud): 36 ms.
$ node string-tagcloud.js
Time (string-tagcloud): 26 ms.
$ node -v
v8.0.0-pre
$
这是静态基准和性能测试套件常见的一个问题。今天,没有人会正儿八经地用 eval 解析 JSON 数据(不仅是因为性能问题,还出于严重的安全性考虑),而是坚持为最近五年写的代码使用 JSON.parse[19]。事实上,使用 eval 解析 JSON 可能会被视作产品级代码的的一个漏洞!所以引擎作者致力于新代码的性能所作的努力并没有反映在这个古老的基准中,相反地,而是使得 eval 不必要地更智能复杂化,从而赢得 string-tagcloud.js 测试。

好吧,让我们看看另一个例子:3d-cube.js[20]。这个基准测试做了很多矩阵运算,即便是最聪明的编译器对此也无可奈何,只能说执行而已。基本上,该基准测试花了大量的时间执行 Loop 函数及其调用的函数。


3d-cube.js
一个有趣的发现是:RotateX、RotateY 和 RotateZ 函数总是调用相同的常量参数 Phi。

3d-cube.js
这意味着我们基本上总是为 Math.sin[21] 和 Math.cos[22] 计算相同的值,每次执行都要计算 204 次。只有 3 个不同的输入值:

◈ 0.017453292519943295
◈ 0.05235987755982989
◈ 0.08726646259971647
显然,你可以在这里做的一件事情就是通过缓存以前的计算值来避免重复计算相同的正弦值和余弦值。事实上,这是 V8 以前的做法,而其它引擎例如 SpiderMonkey 目前仍然在这样做。我们从 V8 中删除了所谓的超载缓存transcendental cache,因为缓存的开销在实际的工作负载中是不可忽视的,你不可能总是在一行代码中计算相同的值,这在其它地方倒不稀奇。当我们在 2013 和 2014 年移除这个特定的基准优化时,我们对 SunSpider 基准产生了强烈的冲击,但我们完全相信,为基准而优化并没有任何意义,并同时以这种方式批判了现实场景中的使用案例。


3d-cube 基准
(来源:arewefastyet.com[23])

显然,处理恒定正弦/余弦输入的更好的方法是一个内联的启发式算法,它试图平衡内联因素与其它不同的因素,例如在调用位置优先选择内联,其中常量叠算constant folding可以是有益的,例如在 RotateX、RotateY 和 RotateZ 调用位置的案例中。但是出于各种原因,这对于 Crankshaft 编译器并不可行。使用 Ignition 和 TurboFan 倒是一个明智的选择,我们已经在开发更好的内联启发式算法[24]。

垃圾回收(GC)是有害的
除了这些非常具体的测试问题,SunSpider 基准测试还有一个根本性的问题:总体执行时间。目前 V8 在适当的英特尔硬件上运行整个基准测试大概只需要 200ms(使用默认配置)。次垃圾回收minor GC在 1ms 到 25ms 之间(取决于新空间中的存活对象和旧空间的碎片),而主垃圾回收major GC暂停的话可以轻松减掉 30ms(甚至不考虑增量标记的开销),这超过了 SunSpider 套件总体执行时间的 10%!因此,任何不想因垃圾回收循环而造成减速 10-20% 的引擎,必须用某种方式确保它在运行 SunSpider 时不会触发垃圾回收。


driver-TEMPLATE.html
就实现而言,有不同的方案,不过就我所知,没有一个在现实场景中产生了任何积极的影响。V8 使用了一个相当简单的技巧:由于每个 SunSpider 套件都运行在一个新的 <iframe> 中,这对应于 V8 中一个新的本地上下文,我们只需检测快速的 <iframe> 创建和处理(所有的 SunSpider 测试每个花费的时间小于 50ms),在这种情况下,在处理和创建之间执行垃圾回收,以确保我们在实际运行测试的时候不会触发垃圾回收。这个技巧运行的很好,在 99.9% 的案例中没有与实际用途冲突;除了时不时的你可能会受到打击,不管出于什么原因,如果你做的事情让你看起来像是 V8 的 SunSpider 测试驱动程序,你就可能被强制的垃圾回收打击到,这有可能对你的应用导致负面影响。所以谨记一点:不要让你的应用看起来像 SunSpider!

我可以继续展示更多 SunSpider 示例,但我不认为这非常有用。到目前为止,应该清楚的是,为刷新 SunSpider 评分而做的进一步优化在现实场景中没有带来任何好处。事实上,世界可能会因为没有 SunSpider 而更美好,因为引擎可以放弃只是用于 SunSpider 的奇淫技巧,或者甚至可以伤害到现实中的用例。不幸的是,SunSpider 仍然被(科技)媒体大量地用来比较他们眼中的浏览器性能,或者甚至用来比较手机!所以手机制造商和安卓制造商对于让 SunSpider(以及其它现在毫无意义的基准 FWIW) 上的 Chrome 看起来比较体面自然有一定的兴趣。手机制造商通过销售手机来赚钱,所以获得良好的评价对于电话部门甚至整间公司的成功至关重要。其中一些部门甚至在其手机中配置在 SunSpider 中得分较高的旧版 V8,将他们的用户置于各种未修复的安全漏洞之下(在新版中早已被修复),而让用户被版本的 V8 带来的任何现实场景的性能优势拒之门外!


Galaxy S7 和 S7 Edge 的评价:三星的高光表现

欢迎加入本站公开兴趣群
软件开发技术群
兴趣范围包括:Java,C/C++,Python,PHP,Ruby,shell等各种语言开发经验交流,各种框架使用,外包项目机会,学习、培训、跳槽等交流
QQ群:26931708

Hadoop源代码研究群
兴趣范围包括:Hadoop源代码解读,改进,优化,分布式系统场景定制,与Hadoop有关的各种开源项目,总之就是玩转Hadoop
QQ群:288410967 

鲜花

握手

雷人

路过

鸡蛋

相关阅读

最新评论

热门频道

  • 大数据
  • 商业智能
  • 量化投资
  • 科学探索
  • 创业

即将开课

 

GMT+8, 2018-11-14 08:46 , Processed in 0.198233 second(s), 24 queries .