mirror of
https://github.com/LCTT/TranslateProject.git
synced 2025-01-25 23:11:02 +08:00
校对完毕 @OneNewLife
This commit is contained in:
parent
1c2e7efee2
commit
89cf7189a5
@ -1,43 +1,43 @@
|
||||
探索传统 JavaScript 基准测试
|
||||
============================================================
|
||||
|
||||
可以很公平地说,[JavaScript][22] 是当下软件工程最为重要的技术。由于在语言设计者看来,JavaScript 不是十分优雅;在编译器工程师看来,它的性能没有多卓越;并且它还没有一个足够庞大的标准库,所以对于深入接触过编程语言、编译器和虚拟机的人来说,这依然是一个惊喜。取决于你和谁吐槽,JavaScript 的缺点花上数周都列举不完,不过你总会找到一些你从所未知的神奇的东西。尽管这看起来明显困难重重,不过 JavaScript 今天还是成为了 web 的核心,并且还成为服务器/云端的主要技术(通过 [Node.js][23]),甚至还开辟了进入物联网领域的道路。
|
||||
可以很公平地说,[JavaScript][22] 是当下软件工程最重要的技术。对于那些深入接触过编程语言、编译器和虚拟机的人来说,这仍然有点令人惊讶,因为在语言设计者看来,`JavaScript` 不是十分优雅;在编译器工程师看来,它没有多少可优化的地方;而且还没有一个伟大的标准库。取决于你和谁吐槽,`JavaScript` 的缺点你花上数周都枚举不完,不过你总会找到一些你从所未知的神奇的东西。尽管这看起来明显困难重重,不过 `JavaScript` 还是成为当今 web 的核心,并且还成为服务器端/云端的主导技术(通过 [Node.js][23]),甚至还开辟了进军物联网空间的道路。
|
||||
|
||||
问题来了,为什么 JavaScript 如此受欢迎/成功?我从未得到一个很好的答案。如今我们有很多理由使用 JavaScript,或许最重要的是围绕其构建的庞大的生态系统以及今天大量可用的资源。但所有这一切实际上是发展到一定程度的结果。为什么 JavaScript 变得流行起来了?嗯,你或许会说,这是 web 多年来的通用语了。但是在很长一段时间里,人们极其讨厌 JavaScript。回顾过去,似乎第一次 JavaScript 浪潮爆发在上个年代的后半段。不出所料,那个时候 JavaScript 引擎在不同的负载下实现了巨大的加速,这可能刷新了很多人对 JavaScript 的看法。
|
||||
问题来了,为什么 `JavaScript` 如此受欢迎/成功?我知道没有一个很好的答案。如今我们有许多使用 `JavaScript` 的好理由,或许最重要的是围绕其构建的庞大的生态系统,以及今天大量可用的资源。但所有这一切实际上是发展到一定程度的后果。为什么 `JavaScript` 变得流行起来了?嗯,你或许会说,这是 web 多年来的通用语了。但是在很长一段时间里,人们极其讨厌 `JavaScript`。回顾过去,似乎第一波 `JavaScript` 浪潮爆发在上个年代的后半段。不出所料,那个时候 `JavaScript` 引擎在不同的负载下实现了巨大的加速,这可能让很多人对 `JavaScript` 刮目相看。
|
||||
|
||||
回到过去那些日子,这些加速测试使用了今天所谓的传统 JavaScript 基准,从苹果的 [SunSpider 基准][24](所有 JavaScript 微基准之母)到 Mozilla 的 [Kraken 基准][25] 和谷歌的 V8 基准。后来,V8 基准被 [Octane 基准][26] 取代,而苹果发布了自家新的 [JetStream 基准][27]。这些传统的 JavaScript 基准测试驱动了令人惊叹的努力,使 JavaScript 的性能达到一个在本世纪初没人能预料到的水平。据报道加速达到了 1000 倍,一夜之间在网站使用 `<script>` 标签不再是魔鬼的舞蹈,做客户端不仅仅是可能的,甚至是被鼓励的。
|
||||
回到过去那些日子,这些加速测试使用了现在所谓的传统 `JavaScript` 基准——从苹果的 [SunSpider 基准][24](JavaScript 微基准之母)到 Mozilla 的 [Kraken 基准][25] 和谷歌的 `V8` 基准。后来,`V8` 基准被 [Octane 基准][26] 取代,而苹果发布了新的 [JetStream 基准][27]。这些传统的 `JavaScript` 基准测试驱动了无数人的努力,使 `JavaScript` 的性能达到了本世纪初没人能预料到的水平。据报道加速达到了 1000 倍,一夜之间在网站使用 `<script>` 标签不再是魔鬼的舞蹈,做客户端不再仅仅是可能的了,甚至是被鼓励的。
|
||||
|
||||
[![性能测试,JS 基准的简史](http://benediktmeurer.de/images/2016/sethcds-20161216.png)][28]
|
||||
|
||||
在 2016 年,所有(相关的)JavaScript 引擎达到了一个令人难以置信的水平,web 应用可以像端应用一样快。引擎配有复杂的优化编译器,通过根据收集的关于类型/形状的反馈来推测某些操作(即属性访问、二进制操作、比较、调用等),生成高度优化的机器代码的短序列。大多数这些优化是由 SunSpider 或 Kraken 等微基准以及 Octane 和 JetStream 等静态测试套件驱动的。由于有基于像 [asm.js][29] 和 [Emscripten][30] 这样的 JavaScript 技术,我们甚至可以将大型 C++ 应用编译成 JavaScript,并在你的浏览器上运行,而无需下载或安装任何东西。例如,现在你可以在 web 上玩 [AngryBots][31],无需沙盒,而过去的 web 游戏需要安装一堆诸如 Adobe Flash 或 Chrome PNaCl 的插件。
|
||||
现在是 2016 年,所有(相关的)`JavaScript` 引擎的性能都达到了一个令人难以置信的水平,web 应用可以像端应用(或者本地的应用)一样快。引擎配有复杂的优化编译器,通过收集过去关于类型/形状的反馈来推测某些操作(即属性访问、二进制操作、比较、调用等),生成高度优化的机器代码的短序列。大多数优化是由 `SunSpider` 或 `Kraken` 等微基准以及 `Octane` 和 `JetStream` 等静态测试套件驱动的。由于有像 [asm.js][29] 和 [Emscripten][30] 这样的 `JavaScript` 技术,我们甚至可以将大型 `C++` 应用程序编译成 `JavaScript`,并在你的浏览器上运行,而无需下载或安装任何东西。例如,现在你可以在 web 上玩 [AngryBots][31],无需沙盒,而过去的 web 游戏需要安装一堆诸如 `Adobe Flash` 或 `Chrome PNaCl` 的插件。
|
||||
|
||||
这些成就绝大多数都要归功于这些微基准和静态性能测试套件,以及这些传统 JavaScript 基准间至关重要的竞争。你可以说你可以用 SunSpider 来干什么,但很显然,没有 SunSpider,JavaScript 的性能可能达不到今天的高度。好的,赞美到此为止。现在看看另一面,所有的静态性能测试——无论是微基准还是大型应用的宏基准,都注定要随着时间的推移变得不相关!为什么?因为在开始游戏前,基准只能教你这么多。一旦达到某个阔值以上(或以下),那么有益于特定基准的优化的一般实用性将呈指数下降。例如,我们将 Octane 作为现实世界中 web 应用性能的代理,并且在相当长一段时间里,它可能做得相当不错,但是现在,Octane 与现实世界中的时间分布是截然不同的,因此当前优化 Octane 以超越自身可能在现实中得不到任何显著的改进(无论是通用 web 还是 Node.js 的工作负载)。
|
||||
这些成就绝大多数都要归功于这些微基准和静态性能测试套件,以及这些传统 `JavaScript` 基准间至关重要的竞争。你可以对 `SunSpider` 表示不满,但很显然,没有 `SunSpider`,`JavaScript` 的性能可能达不到今天的高度。好吧,赞美到此为止。现在看看另一方面,所有静态性能测试——无论是微基准还是大型应用的宏基准,都注定要随着时间的推移变得不相关!为什么?因为在开始游戏前,基准只能教你这么多。一旦达到某个阔值以上(或以下),那么有益于特定基准的优化的一般适用性将呈指数下降。例如,我们将 `Octane` 作为现实世界中 web 应用性能的代理,并且在相当长的一段时间里,它可能做得很不错,但是现在,`Octane` 与现实场景中的时间分布是截然不同的,因此即使眼下再优化 `Octane` 至超越自身,可能在现实世界中还是得不到任何显著的改进(无论是通用 web 还是 `Node.js` 的工作负载)。
|
||||
|
||||
[![基准与现实世界的时间分布对比](http://benediktmeurer.de/images/2016/verwaestblinkon-20161216.png)][32]
|
||||
|
||||
由于传统 JavaScript 基准(包括最新版的 JetStream 和 Octane)可能已经超越其有用性变得越来越明显,我们开始调查新的方法来测量年初(2016 年)真实世界的性能,为 V8 和 Chrome 添加了大量新的性能追踪钩子。我们特意添加了一些机制来查看我们在浏览 web 时花费的时间,即是否是脚本执行、垃圾回收、编译等,并且这些调查的结果非常有趣和令人惊讶。从上面的幻灯片可以看出,运行 Octane 花费超过 70% 的时间执行 JavaScript 和回收垃圾,而浏览 web 的时候,通常执行 JavaScript 花费的时间不到 30%,垃圾回收占用的时间永远不会超过 5%。而花费大量时间来解析和编译,这不像 Octane 的作风。因此,将更多的时间用在优化 JavaScript 执行上将提高你的 Octane 跑分,但不会对加载 [youtube.com][33] 有任何积极的影响。事实上,花费更多的时间来优化 JavaScript 执行甚至可能会损伤你的真实性能,因为编译器需要更多的时间,或者你需要跟踪额外的反馈,从而最终为编译、IC 和运行时桶开销更多的时间。
|
||||
由于传统 `JavaScript` 基准(包括最新版的 `JetStream` 和 `Octane`)可能已经超越其有用性变得越来越明显,我们开始调查新的方法来测量年初现实场景的性能,为 `V8` 和 `Chrome` 添加了大量新的性能追踪钩子。我们还特意添加一些机制来查看我们在浏览 web 时的时间开销,即是否是脚本执行、垃圾回收、编译等,并且这些调查的结果非常有趣和令人惊讶。从上面的幻灯片可以看出,运行 `Octane` 花费超过 70% 的时间执行 `JavaScript` 和回收垃圾,而浏览 web 的时候,通常执行 `JavaScript` 花费的时间不到 30%,垃圾回收占用的时间永远不会超过 5%。反而花费大量时间来解析和编译,这不像 `Octane` 的作风。因此,将更多的时间用在优化 `JavaScript` 执行上将提高你的 `Octane` 跑分,但不会对加载 [youtube.com][33] 有任何积极的影响。事实上,花费更多的时间来优化 `JavaScript` 执行甚至可能有损你现实场景的性能,因为编译器需要更多的时间,或者你需要跟踪更多的反馈,最终为编译、IC 和运行时桶开销更多的时间。
|
||||
|
||||
[![测速表](http://benediktmeurer.de/images/2016/speedometer-20161216.png)][34]
|
||||
|
||||
还有另外一组基准测试用于测试浏览器整体性能(包括 JavaScript 和 DOM 性能),最新推出的是 [Speedometer 基准][35]。基准试图通过运行一个用不同的主流 web 框架实现的简单的 [TodoMVC][36] 应用(现在看来有点过时了,不过新版本正在研发中)以捕获真实性能。各种在 Octane 下的测试(angular、ember、react、vanilla、flight 和 bbackbone)都囊括在幻灯片中,你可以看到这些测试似乎更好地代表了现在的性能指标。但是请注意,这些数据收集与本文撰写将近 6 个月以前,而且我们优化了更多的现实世界模式(例如我们正在重构 IC 系统以显著地减小开销,并且 [解析器也正在重新设计][37])。还要注意的是,虽然这看起来像是只和浏览器相关,但我们有非常强有力的证据表明传统的峰值性能基准也不是现实世界 Node.js 应用性能的一个好代表。
|
||||
还有另外一组基准测试用于测量浏览器整体性能(包括 `JavaScript` 和 `DOM` 性能),最新推出的是 [Speedometer 基准][35]。基准试图通过运行一个用不同的主流 web 框架实现的简单的 [TodoMVC][36] 应用(现在看来有点过时了,不过新版本正在研发中)以捕获真实性能。各种在 Octane 下的测试(Angular、Ember、React、Vanilla、Flight 和 Backbone)都罗列在幻灯片中,你可以看到这些测试似乎更好地代表了现在的性能指标。但是请注意,这些数据收集在本文撰写将近 6 个月以前,而且我们优化了更多的现实场景模式(例如我们正在重构 IC 系统以显著地降低开销,并且 [解析器也正在重新设计][37])。还要注意的是,虽然这看起来像是只和浏览器相关,但我们有非常强有力的证据表明传统的峰值性能基准也不是现实场景中 `Node.js` 应用性能的一个好代理。
|
||||
|
||||
[![Speedometer 和 Octane 对比](http://benediktmeurer.de/images/2016/verwaestblinkon2-20161216.png)][38]
|
||||
|
||||
这些可能已经为大多数人所知了,因此我将用本文剩下的部分强调一些关于我为什么认为这不仅有用,而且对于 JavaScript 社区停止关注某一阔值的静态峰值性能基准测试的健康很关键的具体案例。让我通过一些例子说明 JavaScript 引擎如何做游戏基准。
|
||||
所有这一切可能已经路人皆知了,因此我将用本文剩下的部分强调一些关于我为什么认为这不仅有用,而且对于 `JavaScript` 社区的健康(必须停止关注某一阔值的静态峰值性能基准测试)很关键的具体案例。让我通过一些例子说明 `JavaScript` 引擎怎样来玩弄基准。
|
||||
|
||||
### 臭名昭著的 SunSpider 案例
|
||||
|
||||
一篇关于传统 JavaScript 基准测试的博客文章如果没有指出 SunSpider 的问题是不完整的。让我们从性能测试的最佳例子开始,它在现实世界中不是很使用:[`bitops-bitwise-and.js` 性能测试][39]
|
||||
一篇关于传统 `JavaScript` 基准测试的博客如果没有指出 `SunSpider` 明显的问题是不完整的。让我们从性能测试的最佳实践开始,它在现实场景中不是很适用:[`bitops-bitwise-and.js` 性能测试][39]
|
||||
|
||||
[![bitops-bitwise-and.js](http://benediktmeurer.de/images/2016/bitops-bitwise-and-20161216.png)][40]
|
||||
|
||||
有一些算法需要快速的位运算,特别是从 C/C++ 转译成 JavaScript 的地方,所以它确实有一些意义,能够快速执行此操作.然而,现实世界的网页可能不关心引擎是否可以执行位运算,并且在循环中比另一个引擎快两倍.但是再盯着这段代码几秒钟,你可能会注意到在第一次循环迭代之后 `bitwiseAndValue` 将变成 `0`,并且在接下来的 599999 次迭代中将保持为 `0`。所以一旦你在此获得好的性能,即在体面的硬件上所有测试均低于 5ms,在经过尝试认识到只有循环的第一次是必要的,而剩余的迭代只是在浪费时间(例如 [loop peeling][41] 后面的死代码),你就可以用这个基准来测试游戏了。这需要 JavaScript 中的一些机制来执行这种转换,即你需要检查 `bitwiseAndValue` 是全局对象的常规属性还是在执行脚本之前不存在,全局对象必须没有拦截器,或者它的原型等等,但如果你真的想要赢得这个基准,并且你愿意全身心投入,那么你可以在不到 1ms 的时间内执行这个测试。然而,这种优化将局限于这种特殊情况,并且测试的轻微修改可能不再触发它。
|
||||
有一些算法需要进行快速的位运算,特别是从 `C/C++` 转译成 `JavaScript` 的地方,所以快速执行按位操作确实有点意义。然而,现实场景中的网页可能不关心引擎是否可以执行位运算,并且能否在循环中比另一个引擎快两倍。但是再盯着这段代码几秒钟,你可能会注意到,在第一次循环迭代之后 `bitwiseAndValue` 将变成 `0`,并且在接下来的 599999 次迭代中将保持为 `0`。所以一旦你在此获得好性能,即在体面的硬件上所有测试均低于 5ms,在经过尝试之后意识到,只有循环的第一次是必要的,而剩余的迭代只是在浪费时间(例如 [loop peeling][41] 后面的死代码),现在你可以开始玩弄这个基准了。这需要 JavaScript 中的一些机制来执行这种转换,即你需要检查 `bitwiseAndValue` 是全局对象的常规属性还是在执行脚本之前不存在,全局对象或者它的原型上必须没有拦截器。但如果你真的想要赢得这个基准测试,并且你愿意全力以赴,那么你可以在不到 1ms 的时间内完成这个测试。然而,这种优化将局限于这种特殊情况,并且测试的轻微修改可能不再触发它。
|
||||
|
||||
好的,那么 [`bitops-bitwise-and.js`][42] 测试彻底肯定是微基准最失败的案例。让我们继续转移到 SunSpider 中更真实的世界——[`string-tagcloud.js`][43] 测试,它基本上运行着一个较早版本的 `json.js` polyfill。该测试可以说看起来更合理,但是查看基准的配置之后立刻显示:大量的时间浪费在一条 `eval` 表达式(高达 20% 的总执行时间被用于解析和编译,再加上实际执行编译后代码的 10% 的时间)。
|
||||
好吧,那么 [`bitops-bitwise-and.js`][42] 测试彻底肯定是微基准最失败的案例。让我们继续转移到 SunSpider 中更逼真的场景——[`string-tagcloud.js`][43] 测试,它的底层运行着一个较早版本的 `json.js polyfill`。该测试可以说看起来比位运算测试更合理,但是查看基准的配置之后立刻显示:大量的时间浪费在一条 `eval` 表达式(高达 20% 的总执行时间被用于解析和编译,再加上实际执行编译后代码的 10% 的时间)。
|
||||
|
||||
[![string-tagcloud.js](http://benediktmeurer.de/images/2016/string-tagcloud-20161216.png)][44]
|
||||
|
||||
仔细看看,这个 `eval` 只执行了一次,并传递一个 JSON 格式的字符串,它包含一个由 2501 个含有 `tag` 和 `popularity` 字段的对象组成的数组:
|
||||
仔细看看,这个 `eval` 只执行了一次,并传递一个 `JSON` 格式的字符串,它包含一个由 2501 个含有 `tag` 和 `popularity` 属性的对象组成的数组:
|
||||
|
||||
```
|
||||
([
|
||||
@ -55,7 +55,7 @@
|
||||
},
|
||||
{
|
||||
"tag": "multangularness",
|
||||
"popularity": 368304452
|
||||
"popularity": 368304452任何
|
||||
},
|
||||
{
|
||||
"tag": "Fesapo unventurous",
|
||||
@ -77,7 +77,7 @@
|
||||
])
|
||||
```
|
||||
|
||||
显然,解析这些对象字面量,为其生成本地代码,然后执行该代码的成本很高。将输入的字符串解析为 JSON 并生成适当的对象图的开销将更加低。所以,加快这个基准的一个窍门是模拟 `eval`,并尝试总是将数据首先作为 JSON 解析,然后再回溯到真实的解析、编译、执行,直到尝试读取 JSON 失败(尽管需要一些额外的魔法来跳过括号)。回到 2007 年,这甚至不算是一个坏的手段,因为没有 [`JSON.parse`][45],不过在 2017 年这只是 JavaScript 引擎的技术债,可能会放慢 `eval` 的合法使用。
|
||||
显然,解析这些对象字面量,为其生成本地代码,然后执行该代码的成本很高。将输入的字符串解析为 `JSON` 并生成适当的对象图的开销将更加低廉。所以,加快这个基准测试的一个小把戏就是模拟 `eval`,并尝试总是将数据首先作为 `JSON` 解析,然后再回溯到真实的解析、编译、执行,直到尝试读取 `JSON` 失败(尽管需要一些额外的黑魔法来跳过括号)。早在 2007 年,这甚至不算是一个坏点子,因为没有 [`JSON.parse`][45],不过在 2017 年这只是 `JavaScript` 引擎的技术债,可能会让 `eval` 的合法使用遥遥无期。
|
||||
|
||||
```
|
||||
--- string-tagcloud.js.ORIG 2016-12-14 09:00:52.869887104 +0100
|
||||
@ -93,7 +93,7 @@
|
||||
}
|
||||
```
|
||||
|
||||
事实上,将基准测试更新到现代 JavaScript 会立刻提升性能,正如今天的 V8 LKGR 从 36ms 降到了 26ms,性能足足提升了 30%!
|
||||
事实上,将基准测试更新到现代 `JavaScript` 会立刻提升性能,正如今天的 `V8 LKGR` 从 36ms 降到了 26ms,性能足足提升了 30%!
|
||||
|
||||
```
|
||||
$ node string-tagcloud.js.ORIG
|
||||
@ -105,65 +105,65 @@ v8.0.0-pre
|
||||
$
|
||||
```
|
||||
|
||||
这是静态基准测试和性能测试套件常见的一个问题。今天,没人会认真地使用 `eval` 来解析 JSON 数据(不仅是因为性能问题,还出于显著的安全性考虑),而是坚持为过去五年写的所有代码使用 [`JSON.parse`][46]。事实上,使用 `eval` 来解析 JSON 可能会被视作生产代码中的一个漏洞!所以引擎作者致力于关注新代码的性能没有反映在旧基准中,相反,使得 `eval` 在 `string-tagcloud.js` 中赢得不必要的复杂性是有益的。
|
||||
这是静态基准和性能测试套件常见的一个问题。今天,没有人会正儿八经地用 `eval` 解析 `JSON` 数据(不仅是因为性能问题,还出于严重的安全性考虑),而是坚持为所有代码使用诞生于五年前的 [`JSON.parse`][46]。事实上,使用 `eval` 解析 `JSON` 可能会被视作生产环境的一个漏洞!所以引擎作者致力于新代码的性能所作的努力并没有反映在这个古老的基准中,相反地,使用 `eval` 来赢得 `string-tagcloud.js` 测试是没有必要的。
|
||||
|
||||
好吧,让我们看看另一个例子:[`3d-cube.js`][47]。这个基准做了很多矩阵运算,即使最聪明的编译器都做不了很多,但只是必须要执行它。基本上,基准花了很多时间执行 `loop` 函数以及其调用的函数。
|
||||
好吧,让我们看看另一个例子:[`3d-cube.js`][47]。这个基准测试做了很多矩阵运算,即便是最聪明的编译器仅仅执行这个运算都做不了这么多。基本上,基准测试花了大量的时间执行 `Loop` 函数及其调用的函数。
|
||||
|
||||
[![3d-cube.js](http://benediktmeurer.de/images/2016/3d-cube-loop-20161216.png)][48]
|
||||
|
||||
一个有趣的发现是:`RotateX`、`RotateY` 和 `RotateZ` 函数总是使用相同的常量参数 `Phi`。
|
||||
一个有趣的发现是:`RotateX`、`RotateY` 和 `RotateZ` 函数总是调用相同的常量参数 `Phi`。
|
||||
|
||||
[![3d-cube.js](http://benediktmeurer.de/images/2016/3d-cube-rotate-20161216.png)][49]
|
||||
|
||||
这意味着我们基本上总是计算 [`Math.sin`][50] 和 [`Math.cos`][51] 的相同值,每次执行都要计算 204 次。只有 3 个不同的输入值:
|
||||
这意味着我们基本上总是为 [`Math.sin`][50] 和 [`Math.cos`][51] 计算相同的值,每次执行都要计算 204 次。只有 3 个不同的输入值:
|
||||
|
||||
* 0.017453292519943295,
|
||||
* 0.05235987755982989,
|
||||
* 以及 0.08726646259971647
|
||||
* 0.08726646259971647
|
||||
|
||||
显然,这里你可以做的一件事就是通过缓存先前的计算值来避免重复计算相同的正弦值和余弦值。事实上,这是 V8 以前的做法,而其它引擎例如 SpiderMonkey 仍然这样做。我们从 V8 中删除了所谓的超载缓存,因为缓存的开销在实际工作负载中是不可忽视的,你不总是在一行中计算相同的值,这在其它地方倒不稀奇。当我们在 2013 和 2014 年移除这个基准特定的优化时,我们对 SunSpider 基准产生了强烈的冲击,但我们完全相信,优化基准并没有意义,同时以这种方式批判真实使用案例。
|
||||
显然,你可以在这里做的一件事情就是通过缓存以前的计算值来避免重复计算相同的正弦值和余弦值。事实上,这是 `V8` 以前的做法,而其它引擎例如 `SpiderMonkey` 仍然这样做。我们从 `V8` 中删除了所谓的超载缓存,因为缓存的开销在现实中的工作负载是不可忽视的,你不可能总是在一行代码中计算相同的值,这在其它地方倒不稀奇。当我们在 2013 和 2014 年移除这个特定的基准优化时,我们对 `SunSpider` 基准产生了强烈的冲击,但我们完全相信,优化基准并没有任何意义,同时以这种方式批判现实场景中的使用案例。
|
||||
|
||||
[![3d-cube 基准](http://benediktmeurer.de/images/2016/3d-cube-awfy-20161216.png)][52]
|
||||
|
||||
显然,处理恒定正弦/余弦输入的更好方法是一个内联的启发式计算,它试图平衡内联因素并考虑到其它不同因素,例如在调用位置处优先选择内联,其中恒定折叠可能是有益的,例如在 `RotateX`、`RotateY` 和 `RotateZ` 调用网站。但是出于各种原因这对于 Crankshaft 编译器并不真的是可行的。使用 Ignition 和 TurboFan 将是一个明智的选择,我们已经在做更好的 [内联启发式计算][53]。
|
||||
显然,处理恒定正弦/余弦输入的更好的方法是一个内联的启发式算法,它试图平衡内联因素与其它不同的因素,例如在调用位置优先选择内联,其中恒定折叠可以是有益的,例如在 `RotateX`、`RotateY` 和 `RotateZ` 中调用边界值的案例。但是出于各种原因,这对于 `Crankshaft` 编译器并不可行。使用 `Ignition` 和 `TurboFan` 倒是一个明智的选择,我们已经在开发更好的[内联启发式算法][53]。
|
||||
|
||||
### 垃圾回收是有害的
|
||||
|
||||
除了这些非常特定的测试问题,SunSpider 还有另一个根本的问题:总体执行时间。V8 在体面的英特尔硬件上运行整个基准测试目前大概只需要 200ms(使用默认配置)。次要的 GC 在 1ms 到 25ms 之间(取决于新空间中的活对象和旧空间的碎片),而主 GC 暂停可以浪费 30ms(甚至不考虑增量标记的开销),这超过了 SunSpider 总体运行时间的 10%!因此,任何不想承受由于 GC 循环而造成 10-20% 的减速的引擎,必须以某种方式确保它在运行 SunSpider 时不会触发 GC。
|
||||
除了这些非常具体的测试问题,`SunSpider` 还有一个根本的问题:总体执行时间。目前 `V8` 在体面的英特尔硬件上运行整个基准测试大概只需要 200ms(使用默认配置)。次要的 `GC` 在 1ms 到 25ms 之间(取决于新空间中的活对象和旧空间的碎片),而主 `GC` 暂停可以浪费掉 30ms(甚至不考虑增量标记的开销),这超过了 `SunSpider` 套件总体执行时间的 10%!因此,任何不想因 `GC` 循环而造成减速 10-20% 的引擎,必须用某种方式确保它在运行 `SunSpider` 时不会触发 `GC`。
|
||||
|
||||
[![driver-TEMPLATE.html](http://benediktmeurer.de/images/2016/sunspider-driver-20161216.png)][54]
|
||||
|
||||
有不同的技巧来实现这个想法,不过就我所知,没有一个在现实世界中有任何积极的影响。V8 使用了一个相当简单的技巧:由于每个 SunSpider 测试运行在一个新的 `<iframe>` 中,这对应于 V8 中一个新的本地上下文,我们只需检测快速的 `<iframe>` 创建和处理(所有的 SunSpider 测试花费的时间小于 50ms),在这种情况下,在处理和创建之间执行垃圾回收,以确保我们不会触发 GC,而实际运行测试。这个技巧很好,99.9% 的情况下不会与真正的用途冲突;除了每一个非常时刻,它可以给你强烈冲击,如果无论什么原因,你做的东西,让你看起来像是 V8 的 SunSpider 测试驱动者,然后你就可以得到强制 GC 的强烈冲击,这对你的应用可能会有负面影响。所以紧记一点:**不要让你的应用看起来像 SunSpider!**
|
||||
就实现而言,有不同的方案,不过就我所知,没有一个在现实场景中产生了任何积极的影响。`V8` 使用了一个相当简单的技巧:由于每个 `SunSpider` 套件都运行在一个新的 `<iframe>` 中,这对应于 `V8` 中一个新的本地上下文,我们只需检测快速的 `<iframe>` 创建和处理(所有的 `SunSpider` 测试花费的时间小于 50ms),在这种情况下,在处理和创建之间执行垃圾回收,以确保我们在实际运行测试的时候不会触发 `GC`。这个技巧很好,99.9% 的案例没有与实际用途冲突;除了每时每刻,无论你在做什么,都让你看起来像是 `V8` 的 `SunSpider` 测试驱动程序,那么你可能会遇到困难,或许你可以通过强制 `GC` 来解决,不过这对你的应用可能会有负面影响。所以紧记一点:**不要让你的应用看起来像 SunSpider!**
|
||||
|
||||
我可以继续展示更多的 SunSpider 示例,但我不认为这非常有用。到目前为止,应该清楚的是,对于 SunSpider 的进一步优化,超过良好性能的阔值将不会反映真实世界的任何好处。事实上,世界可能会从 SunSpider 消失中受益,因为引擎可以放弃只是用于 SunSpider 的奇淫技巧,甚至可以伤害到真实用例。不幸的是,SunSpider 仍然被(科技)媒体大量使用来比较他们所认为的浏览器性能,或者甚至用来比较收集!所以手机制造商和安卓制造商对于让 SunSpider(以及其它现在毫无意义的基准 FWIW) 上的 Chrome 看起来比较体面自然有一定的兴趣。手机制造商通过销售手机来赚钱,所以获得良好的评价对于电话部门甚至整间公司的成功至关重要。其中一些部门甚至在其手机中配置在 SunSpider 中得分较高的旧版 V8,向他们的用户展示各种未修复的安全漏洞(在新版中早已被修复),并保护用户免受最新版本的 V8 的任何真实世界的性能优势!
|
||||
我可以继续展示更多 `SunSpider` 示例,但我不认为这非常有用。到目前为止,应该清楚的是,`SunSpider` 为刷新业绩而做的进一步优化在现实场景中没有带来任何好处。事实上,世界可能会因为没有 `SunSpider` 而更美好,因为引擎可以放弃只是用于 `SunSpider` 的奇淫技巧,甚至可以伤害到现实中的用例。不幸的是,SunSpider 仍然被(科技)媒体大量地用来比较他们眼中的浏览器性能,或者甚至用来比较手机!所以手机制造商和安卓制造商对于让 `SunSpider`(以及其它现在毫无意义的基准 `FWIW`) 上的 `Chrome` 看起来比较体面自然有一定的兴趣。手机制造商通过销售手机来赚钱,所以获得良好的评价对于电话部门甚至整间公司的成功至关重要。其中一些部门甚至在其手机中配置在 `SunSpider` 中得分较高的旧版 `V8`,向他们的用户展示各种未修复的安全漏洞(在新版中早已被修复),并保护用户免受最新版本的 `V8` 的任何现实场景的性能优势!
|
||||
|
||||
[![Galaxy S7 和 S7 Edge 的评价:三星的高光表现](http://benediktmeurer.de/images/2016/engadget-20161216.png)][55]
|
||||
|
||||
作为 JavaScript 社区的一员,如果我们真的想认真对待 JavaScript 领域的真实世界性能,我们需要使技术新闻停止使用传统的 JavaScript 基准来比较浏览器或手机。我看到有一个好处是能够在每个浏览器中运行一个基准,并比较它的数量,但是请使用一个基准与今天的事物是相关的,例如真实的 web 页面;如果你觉得需要通过浏览器基准来比较两部手机,请至少考虑使用 [Speedometer][56]。
|
||||
作为 `JavaScript` 社区的一员,如果我们真的想认真对待 `JavaScript` 领域现实场景的性能,我们需要让各大技术媒体停止使用传统的 `JavaScript` 基准来比较浏览器或手机。我看到的一个好处是能够在每个浏览器中运行一个基准测试,并比较它的得分,但是请使用一个与当今世界相关的基准,例如真实的 `web` 页面;如果你觉得需要通过浏览器基准来比较两部手机,请至少考虑使用 [Speedometer][56]。
|
||||
|
||||
### 轻松一刻
|
||||
|
||||
![](http://images-cdn.9gag.com/photo/avZd9NX_700b.jpg)
|
||||
|
||||
我总是喜欢这个在 [Myles Borins][57] 的谈话,所以我不得不无情地偷取了他的想法。所以现在我们从 SunSpider 的咆哮中回过头来,让我们继续检查其它经典基准。
|
||||
我一直很喜欢这个 [Myles Borins][57] 谈话,所以我不得不无耻地向他偷师。现在我们从 `SunSpider` 的谴责中回过头来,让我们继续检查其它经典基准。
|
||||
|
||||
### 不是那么明显的 Kraken 案例
|
||||
### 不是那么详细的 Kraken 案例
|
||||
|
||||
Kraken 基准是 [Mozilla 于 2010 年 9 月 发布的][58],据说它包含了真实世界应用的片段/内核,并且与 SunSpider 相比少了一个微基准。我不想花太多时间在 Kraken 上,因为我认为它不像 SunSpider 和 Octane 一样对 JavaScript 性能有着深远的影响,所以我将强调一个特别的案例——[`audio-oscillator.js`][59] 测试。
|
||||
`Kraken` 基准是 [Mozilla 于 2010 年 9 月 发布的][58],据说它包含了现实场景应用的片段/内核,并且与 `SunSpider` 相比少了一个微基准。我不想花太多时间在 `Kraken` 上,因为我认为它不像 `SunSpider` 和 `Octane` 一样对 `JavaScript` 性能有着深远的影响,所以我将强调一个特别的案例——[`audio-oscillator.js`][59] 测试。
|
||||
|
||||
[![audio-oscillator.js](http://benediktmeurer.de/images/2016/audio-oscillator-20161216.png)][60]
|
||||
|
||||
正如你所见,测试调用 `calcOsc` 函数 500 次。`calcOsc` 首先在全局的正弦 `Oscillator` 上调用 `generate`,然后创建一个新的 `Oscillator`,调用 `generate` 并将其添加到全局正弦的振荡器。没有详细说明测试为什么是这样做的,让我们看看 `Oscillator` 原型上的 `generate` 方法。
|
||||
正如你所见,测试调用了 `calcOsc` 函数 500 次。`calcOsc` 首先在全局的正弦 `Oscillator` 上调用 `generate`,然后创建一个新的 `Oscillator`,调用 `generate` 并将其添加到全局正弦的 `oscillator`。没有详细说明测试为什么是这样做的,让我们看看 `Oscillator` 原型上的 `generate` 方法。
|
||||
|
||||
[![audio-oscillator-data.js](http://benediktmeurer.de/images/2016/audio-oscillator-generate-20161216.png)][61]
|
||||
|
||||
看看代码,你会期望这被数组访问或乘法循环中的 [`Math.round`][62] 调用所主导,但令人惊讶的是 `offset % this.waveTableLength` 表达式完全支配了 `Oscillator.prototype.generate` 的运行时。在任何的英特尔机器上的分析器中运行此基准测试显示,超过 20% 的通过都归功于我们为模数生成的 `idiv` 指令。然而一个有趣的发现是,`Oscillator` 实例的 `waveTableLength` 字段总是包含相同的值——2048,因为它在 `Oscillator` 构造器中只分配一次。
|
||||
看看代码,你会期望这是被循环中的数组访问或者乘法或者 [`Math.round`][62] 调用所主导的,但令人惊讶的是 `offset % this.waveTableLength` 表达式完全支配了 `Oscillator.prototype.generate` 的运行。在任何的英特尔机器上的分析器中运行此基准测试显示,超过 20% 的通过数据都归功于我们为模数生成的 `idiv` 指令。然而一个有趣的发现是,`Oscillator` 实例的 `waveTableLength` 字段总是包含相同的值——2048,因为它在 `Oscillator` 构造器中只分配一次。
|
||||
|
||||
[![audio-oscillator-data.js](http://benediktmeurer.de/images/2016/audio-oscillator-constructor-20161216.png)][63]
|
||||
|
||||
如果我们知道整数模数运算的右边是 2 的幂,我们可以生成[更好的代码][64],显然完全避免了英特尔上的 `idiv` 指令。所以我们需要的是一个方法来获取信息,从 `Oscillator` 构造器到 `Oscillator.prototype.generate` 中的模运算,`this.waveTableLength` 的值总是 2048。一个显而易见的方法是尝试依赖内联的一切进入 `calcOsc` 函数,并让 load/store 消除为我们的常数传播,但这不会为正弦振荡器工作,这只是分配在 `calcOsc` 函数。
|
||||
如果我们知道整数模数运算的右边是 2 的幂,我们可以生成[更好的代码][64],显然完全避免了英特尔上的 `idiv` 指令。所以我们需要获取一种信息使 `this.waveTableLength` 从 `Oscillator` 构造器到 `Oscillator.prototype.generate` 中的模运算都是 2048。一个显而易见的方法是尝试依赖于将所有内容内嵌到 `calcOsc` 函数,并让 `load/store` 消除为我们进行的常量传播,但这对于在 `calcOsc` 函数之外分配的正弦振荡器无效。
|
||||
|
||||
因此,我们做的是添加支持跟踪某些常数值作为模运算符的右侧反馈。这在 V8 中是有意义的,因为我们为诸如 `+`、`*` 和 `%` 的二进制操作跟踪类型反馈,这意味着操作者跟踪输入的类型和产生的输出类型(参见最近圆桌讨论关于 [动态语言的快速运算][65] 的幻灯片)。当然,用 fullcodegen 和 Crankshaft 挂接起来也是相当容易的,`MOD` 的 `BinaryOpIC` 也可以跟踪两个右边的已知权。
|
||||
因此,我们所做的就是添加支持跟踪某些常数值作为模运算符的右侧反馈。这在 `V8` 中是有意义的,因为我们为诸如 `+`、`*` 和 `%` 的二进制操作跟踪类型反馈,这意味着操作者跟踪输入的类型和产生的输出类型(参见最近圆桌讨论关于[动态语言的快速运算][65]的幻灯片)。当然,用 `fullcodegen` 和 `Crankshaft` 挂接起来也是相当容易的,`MOD` 的 `BinaryOpIC` 也可以跟踪两个右边的已知权。
|
||||
|
||||
```
|
||||
$ ~/Projects/v8/out/Release/d8 --trace-ic audio-oscillator.js
|
||||
@ -173,7 +173,7 @@ $ ~/Projects/v8/out/Release/d8 --trace-ic audio-oscillator.js
|
||||
$
|
||||
```
|
||||
|
||||
实际上运行默认配置的 V8(的 Crankshaft 和 fullcodegen)显示 `BinaryOpIC` 正在为模数的右侧拾取恰当的恒定反馈,并且正确地跟踪左手边总是一个小整数(V8 中的 `Smi`),我们也总是生成一个小的整数结果。查看生成的代码使用 `--print-opt-code --code-comments` 快速显示,Crankshaft 利用反馈在 `Oscillator.prototype.generate` 中生成一个高效的代码序列的整数模数:
|
||||
显示表明 `BinaryOpIC` 正在为模数的右侧拾取适当的恒定反馈,并正确跟踪左侧始终是一个小整数(`V8` 的 `Smi` 说),我们也总是产生一个小的整数结果 。 使用 `--print-opt-code -code-comments` 查看生成的代码,很快就显示出,`Crankshaft` 利用反馈在 `Oscillator.prototype.generate` 中为整数模数生成一个有效的代码序列:
|
||||
|
||||
```
|
||||
[...SNIP...]
|
||||
@ -197,11 +197,11 @@ $
|
||||
[...SNIP...]
|
||||
```
|
||||
|
||||
所以你看到我们加载 `this.waveTableLength`(`rbx` 持有 `this` 的引用)的值,检查它仍然是 2048(十六进制的 0x800),如果是这样,只是执行一个位和适当的掩码 0x7ff(`r11` 包含循环感应变量 `i` 的值),而不是使用 `idiv` 指令(注意保留左侧的符号)。
|
||||
所以你看到我们加载 `this.waveTableLength`(`rbx` 持有 `this` 的引用)的值,检查它仍然是 2048(十六进制的 0x800),如果是这样,只是执行一个按位操作和适当的掩码 0x7ff(`r11` 包含循环感应变量 `i` 的值),而不是使用 `idiv` 指令(注意保留左侧的符号)。
|
||||
|
||||
### 过度专业化的问题
|
||||
|
||||
所以这个技巧酷毙了,但正如许多基准关注的技巧都有一个主要的缺点:太过于专业了!一旦右手侧发生变化,所有优化过的代码需要重新优化(假设右手始终是两个不再拥有的权),任何进一步的优化尝试都必须再次使用 `idiv`,因为 `BinaryOpIC` 很可能以 `Smi * Smi -> Smi` 的形式报告反馈。例如,假设我们实例化另一个 `Oscillator`,在其上设置不同的 `waveTableLength`,并为振荡器调用 `generate`,那么即使实际上有趣的 `Oscillator` 不受影响,我们也会损失 20% 的性能(例如,引擎在这里实行非局部惩罚)。
|
||||
所以这个技巧酷毙了,但正如许多基准关注的技巧都有一个主要的缺点:太过于专业了!一旦右侧发生变化,所有优化过的代码需要重构(假设右手始终是两个不再拥有的权),任何进一步的优化尝试都必须再次使用 `idiv`,因为 `BinaryOpIC` 很可能以 `Smi * Smi -> Smi` 的形式报告反馈。例如,假设我们实例化另一个 `Oscillator`,在其上设置不同的 `waveTableLength`,并为振荡器调用 `generate`,那么即使实际上有趣的 `Oscillator` 不受影响,我们也会损失 20% 的性能(例如,引擎在这里实行非局部惩罚)。
|
||||
|
||||
```
|
||||
--- audio-oscillator.js.ORIG 2016-12-15 22:01:43.897033156 +0100
|
||||
@ -218,7 +218,7 @@ $
|
||||
sine.generate();
|
||||
```
|
||||
|
||||
将原始的 `audio-oscillator.js` 的执行时间与包含额外未使用的 `Oscillator` 实例与修改的 `waveTableLength` 的版本进行比较,可以显示预期的结果:
|
||||
将原始的 `audio-oscillator.js` 执行时间与包含额外未使用的 `Oscillator` 实例与修改的 `waveTableLength` 的版本进行比较,可以显示预期的结果:
|
||||
|
||||
```
|
||||
$ ~/Projects/v8/out/Release/d8 audio-oscillator.js.ORIG
|
||||
@ -228,9 +228,9 @@ Time (audio-oscillator-once): 81 ms.
|
||||
$
|
||||
```
|
||||
|
||||
这是一个非常可怕的性能悬崖的例子:让我们说开发人员为库编写代码,并使用某些样本输入值进行仔细的调整和优化,性能是体面的。现在,用户开始使用该库读取性能笔记,但不知何故从性能悬崖下降,因为她/他正在以一种稍微不同的方式使用库,即以某种方式污染某个 `BinaryOpIC` 的类型反馈,并且受到减速 20% 的打击(与该库作者的测量相比),该库的作者和用户都无法解释,这似乎是任意的。
|
||||
这是一个非常可怕的性能悬崖的例子:假设开发人员为库编写代码,并使用某些样本输入值进行仔细的调整和优化,性能是体面的。现在,用户开始使用该库读取性能日志,但不知何故从性能悬崖下降,因为她/他正在以一种稍微不同的方式使用库,即以某种方式污染某种 `BinaryOpIC` 的类型反馈,并且遭受 20% 的减速(与该库作者的测量相比),该库的作者和用户都无法解释,这似乎是随机的。
|
||||
|
||||
现在这在 JavaScript 的领域并不少见,不幸的是,这些悬崖中有几个是不可避免的,因为它们是由于 JavaScript 的性能是基于乐观的假设和猜测的事实。我们已经花了 **很多** 时间和精力来试图找到避免这些性能悬崖的方法,并且仍然提供(几乎)相同的性能。事实证明,尽可能避免 `idiv`,即使你不一定知道右边总是一个 2 的幂(通过动态反馈),所以为什么 TurboFan 的做法有异于 Crankshaft 的做法,因为它总是在运行时检查输入是否是 2 的幂,所以一般情况下,对于有符整数模数,优化两个右手侧的(未知)权看起来像这样(在伪代码中):
|
||||
现在这在 `JavaScript` 领域并不少见,不幸的是,这些悬崖中有几个是不可避免的,因为它们是由于 `JavaScript` 的性能是基于乐观的假设和猜测的事实。我们已经花了 **大量** 时间和精力来试图找到避免这些性能悬崖的方法,不过依旧提供(几乎)相同的性能。事实证明,尽可能避免 `idiv` 是很有意义的,即使你不一定知道右边总是一个 2 的幂(通过动态反馈),所以为什么 `TurboFan` 的做法有异于 `Crankshaft` 的做法,因为它总是在运行时检查输入是否是 2 的幂,所以一般情况下,对于有符整数模数,优化两个右手侧的(未知)权看起来像这样(在伪代码中):
|
||||
|
||||
```
|
||||
if 0 < rhs then
|
||||
@ -249,7 +249,7 @@ else
|
||||
zero
|
||||
```
|
||||
|
||||
这导致更加一致的和可预测的性能(与 TurboFan):
|
||||
这产生更加一致和可预测的性能(使用 `TurboFan`):
|
||||
|
||||
```
|
||||
$ ~/Projects/v8/out/Release/d8 --turbo audio-oscillator.js.ORIG
|
||||
@ -259,21 +259,21 @@ Time (audio-oscillator-once): 69 ms.
|
||||
$
|
||||
```
|
||||
|
||||
基准和过度专业化的问题是,基准可以给你提示在哪里看和做什么,但它不告诉你你要走多元,不能正确保护优化。例如,所有 JavaScript 引擎都使用基准来防止性能下降,但是运行 Kraken 不能保护我们在 TurboFan 中的一般方法,即我们可以将 TurboFan 中的模优化降级到过度专业版本的 Crankshaft,而基准不会告诉我们却在倒退的事实,因为从基准的角度来看这很好!现在你可以扩展基准,也许以上面我们做的相同的方式,并试图用基准覆盖一切,这是引擎实现者在一定程度上做的事情,但这种方法不会任意缩放。即使基准测试方便,易于用来沟通和竞争,以常识所见你还是需要留下空间,否则过度专业化将支配一切,你会有一个真正的、可接受的、巨大的性能悬崖线。
|
||||
基准和过度专业化的问题在于基准可以给你提示在哪里可以看看以及该怎么做,但它不告诉你你应该走多远,不能保护优化。例如,所有 `JavaScript` 引擎都使用基准来防止性能下降,但是运行 `Kraken` 不能保护我们在 `TurboFan` 中的一般方法,即我们可以将 `TurboFan` 中的模优化降级到过度专业版本的 `Crankshaft`,而基准不会告诉我们却在倒退的事实,因为从基准的角度来看这很好!现在你可以扩展基准,也许以上面我们做的相同的方式,并试图用基准覆盖一切,这是引擎实现者在一定程度上做的事情,但这种方法不会任意缩放。即使基准测试方便,易于用来沟通和竞争,以常识所见你还是需要留下空间,否则过度专业化将支配一切,你会有一个真正的、可接受的、巨大的性能悬崖线。
|
||||
|
||||
Kraken 测试还有许多其它的问题,不过现在让我们继续讨论过去五年中最有影响力的 JavaScript 基准测试—— Octane 测试。
|
||||
`Kraken` 测试还有许多其它的问题,不过现在让我们继续讨论过去五年中最有影响力的 `JavaScript` 基准测试—— `Octane` 测试。
|
||||
|
||||
### 仔细看看 Octane
|
||||
### 深入接触 Octane
|
||||
|
||||
[Octane][66] 基准是 V8 基准的继承者,最初[由谷歌于 2012 年中期发布][67],目前的版本 Octane 2.0 [与 2013 年年底发布][68]。这个版本包含 15 个独立测试,其中对于 Splay 和 Mandreel,我们用来测试吞吐量和延迟。这些测试范围从 [微软 TypeScript 编译器][69] 编译自身到 zlib 测试测量原生的 [asm.js][70] 性能,再到 RegExp 引擎的性能测试、光线追踪器、2D 物理引擎等。有关各个基准测试项的详细概述,请参阅[说明书][71]。所有这些测试项目都经过仔细的筛选,以反映 JavaScript 性能的方方面面,我们认为这在 2012 年非常重要,或许预计在不久的将来会变得更加重要。
|
||||
[Octane][66] 基准是 `V8` 基准的继承者,最初由[谷歌于 2012 年中期发布][67],目前的版本 `Octane` 2.0 [于 2013 年年底发布][68]。这个版本包含 15 个独立测试,其中对于 `Splay` 和 `Mandreel`,我们用来测试吞吐量和延迟。这些测试范围从 [微软 TypeScript 编译器][69] 编译自身到 `zlib` 测试测量原生的 [asm.js][70] 性能,再到 `RegExp` 引擎的性能测试、光线追踪器、2D 物理引擎等。有关各个基准测试项的详细概述,请参阅[说明书][71]。所有这些测试项目都经过仔细的筛选,以反映 `JavaScript` 性能的方方面面,我们认为这在 2012 年非常重要,或许预计在不久的将来会变得更加重要。
|
||||
|
||||
在很大程度上 Octane 在实现其将 JavaScript 性能提高到更高水平的目标方面无比成功,它在 2012 年和 2013 年引导了良性的竞争,Octane 带来了巨大的业绩的成就。但是现在几乎是 2017 年,世界看起来与 2012 年真的迥然不同了。除了通常的和经常引用的批评,Octane 中的大多数项目基本上已经过时(例如,老版本的 TypeScript,zlib 通过老版本的 [Emscripten][72] 编译而成,Mandreel 甚至不再可用等等),某种更重要的方式影响了 Octane 的用途:
|
||||
在很大程度上 `Octane` 在实现其将 `JavaScript` 性能提高到更高水平的目标方面无比的成功,它在 2012 年和 2013 年引导了良性的竞争,`Octane` 创造了巨大的业绩和成就。但是现在将近 2017 年了,世界看起来与 2012 年真的迥然不同了。除了通常和经常被引用的批评,`Octane` 中的大多数项目基本上已经过时(例如,老版本的 `TypeScript`,`zlib` 通过老版本的 [Emscripten][72] 编译而成,`Mandreel` 甚至不再可用等等),某种更重要的方式影响了 `Octane` 的用途:
|
||||
|
||||
我们看到大型 web 框架赢得了 web 种族之争,尤其是像 [Ember][73] 和 [AngularJS][74] 这样的重型框架,它们使用了 JavaScript 执行模式,不过根本没有被 Octane 所反映,并且经常受到(我们)Octane 具体优化的损害。我们还看到 JavaScript 在服务器和工具前端获胜,这意味着有大规模的 JavaScript 应用现在通常运行上数星期,如果不是运行上数年都不会被 Octane 捕获。正如开篇所述,我们有硬数据表明 Octane 的执行和内存配置文件与我们每天在 web 上看到的截然不同。
|
||||
我们看到大型 web 框架赢得了 web 种族之争,尤其是像 [Ember][73] 和 [AngularJS][74] 这样的重型框架,它们使用了 `JavaScript` 执行模式,不过根本没有被 `Octane` 所反映,并且经常受到(我们)`Octane` 具体优化的损害。我们还看到 `JavaScript` 在服务器和工具前端获胜,这意味着有大规模的 `JavaScript` 应用现在通常运行上数星期,如果不是运行上数年都不会被 `Octane` 捕获。正如开篇所述,我们有硬数据表明 `Octane` 的执行和内存配置文件与我们每天在 web 上看到的截然不同。
|
||||
|
||||
让我们来看看今天 Octane 一些基准游戏的具体例子,其中优化不再反映在现实世界。请注意,即使这可能听起来有点负面回顾,它绝对不意味着这样!正如我已经说过好几遍,Octane 是 JavaScript 性能故事中的重要一章,它发挥了非常重要的作用。在过去由 Octane 驱动的 JavaScript 引擎中的所有优化都是善意地添加的,因为 Octane 是真实世界性能的好代理!每个年代都有它的基准,而对于每一个基准都有一段时间你必须要放手!
|
||||
让我们来看看今天一些玩弄 `Octane` 基准的具体例子,其中优化不再反映在现实场景。请注意,即使这可能听起来有点负面回顾,它绝对不意味着这样!正如我已经说过好几遍,`Octane` 是 `JavaScript` 性能故事中的重要一章,它发挥了至关重要的作用。在过去由 `Octane` 驱动的 `JavaScript` 引擎中的所有优化都是善意地添加的,因为 `Octane` 是现实场景性能的好代理!每个年代都有它的基准,而对于每一个基准都有一段时间你必须要放手!
|
||||
|
||||
话虽这么说,让我们在路上看这个节目,首先看看 Box2D 测试,它是基于 [Box2DWeb][75] (一个最初由 Erin Catto 写的移植到 JavaScript 的流行的 2D 物理引擎)的。总的来说,很多浮点数学驱动了很多 JavaScript 引擎下很好的优化,但是,事实证明它包含一个可以肆意玩弄基准的漏洞(怪我,我发现了漏洞,并添加在这种情况下的漏洞)。在基准中有一个函数 `D.prototype.UpdatePairs`,看起来像这样:
|
||||
话虽如此,让我们在路上看这个节目,首先看看 `Box2D` 测试,它是基于 [Box2DWeb][75] (一个最初由 Erin Catto 编写的移植到 `JavaScript` 的流行的 2D 物理引擎)的。总的来说,很多浮点数学驱动了很多 `JavaScript` 引擎下很好的优化,但是,事实证明它包含一个可以肆意玩弄基准的漏洞(怪我,我发现了漏洞,并添加在这种情况下的漏洞)。在基准中有一个函数 `D.prototype.UpdatePairs`,看起来像这样:
|
||||
|
||||
```
|
||||
D.prototype.UpdatePairs = function(b) {
|
||||
@ -308,7 +308,7 @@ D.prototype.UpdatePairs = function(b) {
|
||||
};
|
||||
```
|
||||
|
||||
一些分析显示,在第一个循环中传递给 `e.m_tree.Query` 的无害的内部函数花费了大量的时间:
|
||||
一些分析显示,在第一个循环中传递给 `e.m_tree.Query` 的无辜的内部函数花费了大量的时间:
|
||||
|
||||
```
|
||||
function(t) {
|
||||
@ -333,7 +333,7 @@ x.proxyA = t < m ? t : m;
|
||||
x.proxyB = t >= m ? t : m;
|
||||
```
|
||||
|
||||
所以这两行无辜的代码要负起 99% 的时间开销的责任!这怎么来的?好吧,与 JavaScript 中的许多东西一样,[抽象关系比较][79] 的直观用法不一定是正确的。在这个函数中,`t` 和 `m` 都是 `L` 的实例,它是这个应用的一个中心类,但不会覆盖 `Symbol.toPrimitive`、`“toString”`、`“valueOf”` 或 `Symbol.toStringTag` 属性,它们与抽象关系比较相关。所以如果你写 `t < m` 会发生什么呢?
|
||||
所以这两行无辜的代码要负起 99% 的时间开销的责任!这怎么来的?好吧,与 `JavaScript` 中的许多东西一样,[抽象关系比较][79] 的直观用法不一定是正确的。在这个函数中,`t` 和 `m` 都是 `L` 的实例,它是这个应用的一个中心类,但不会覆盖 `Symbol.toPrimitive`、`“toString”`、`“valueOf”` 或 `Symbol.toStringTag` 属性,它们与抽象关系比较相关。所以如果你写 `t < m` 会发生什么呢?
|
||||
|
||||
1. 调用 [ToPrimitive][12](`t`, `hint Number`)。
|
||||
2. 运行 [OrdinaryToPrimitive][13](`t`, `"number"`),因为这里没有 `Symbol.toPrimitive`。
|
||||
@ -363,7 +363,7 @@ x.proxyB = t >= m ? t : m;
|
||||
},
|
||||
```
|
||||
|
||||
由于这样做会跳过比较来达到 13% 的惊人加速,并且所有的属性查找和内置函数的调用都会被触发。
|
||||
因为这样做会跳过比较以达到 13% 的惊人的性能提升,并且所有的属性查找和内置函数的调用都会被它触发。
|
||||
|
||||
```
|
||||
$ ~/Projects/v8/out/Release/d8 octane-box2d.js.ORIG
|
||||
@ -373,7 +373,7 @@ Score (Box2D): 55359
|
||||
$
|
||||
```
|
||||
|
||||
那么我们是怎么做的呢?事实证明,我们已经有一种用于跟踪比较对象的形状的机制,比较发生于 `CompareIC`,即所谓的已知接收器映射跟踪(其中的映射是 V8 的对象形状+原型),不过这是有限的抽象和严格相等比较。但是我可以很容易地扩展跟踪,并且收集反馈进行抽象的关系比较:
|
||||
那么我们是怎么做呢?事实证明,我们已经有一种用于跟踪比较对象的形状的机制,比较发生于 `CompareIC`,即所谓的已知接收器映射跟踪(其中的映射是 `V8` 的对象形状+原型),不过这是有限的抽象和严格相等比较。但是我可以很容易地扩展跟踪,并且收集反馈进行抽象的关系比较:
|
||||
|
||||
```
|
||||
$ ~/Projects/v8/out/Release/d8 --trace-ic octane-box2d.js
|
||||
@ -384,7 +384,7 @@ $ ~/Projects/v8/out/Release/d8 --trace-ic octane-box2d.js
|
||||
$
|
||||
```
|
||||
|
||||
这里基准代码中使用的 `CompareIC` 告诉我们,对于我们正在查看的函数中的 LT(小于)和 GTE(大于或等于)比较,到目前为止这只能看到`接收器`(V8 的 JavaScript 对象),并且所有这些接收器具有相同的映射 `0x1d5a860493a1`,其对应于 `L` 实例的映射。因此,在优化的代码中,只要我们知道比较的两侧映射的结果都为 `0x1d5a860493a1`,并且没人混淆 `L` 的原型链(即 `Symbol.toPrimitive`、`"valueOf"` 和 `"toString"` 这些方法都是默认的,并且没人赋予过 `Symbol.toStringTag` 的访问权限),我们可以将这些操作分别常量折叠为 `false` 和 `true`。剩下的故事都是关于 Crankshaft 的黑魔法,有很多一部分都是由于初始化的时候忘记正确地检查 `Symbol.toStringTag` 属性:
|
||||
这里基准代码中使用的 `CompareIC` 告诉我们,对于我们正在查看的函数中的 `LT`(小于)和 `GTE`(大于或等于)比较,到目前为止这只能看到 `RECEIVERs`(接收器,`V8` 的 `JavaScript` 对象),并且所有这些接收器具有相同的映射 `0x1d5a860493a1`,其对应于 `L` 实例的映射。因此,在优化的代码中,只要我们知道比较的两侧映射的结果都为 `0x1d5a860493a1`,并且没人混淆 `L` 的原型链(即 `Symbol.toPrimitive`、`"valueOf"` 和 `"toString"` 这些方法都是默认的,并且没人赋予过 `Symbol.toStringTag` 的访问权限),我们可以将这些操作分别常量折叠为 `false` 和 `true`。剩下的故事都是关于 `Crankshaft` 的黑魔法,有很多一部分都是由于初始化的时候忘记正确地检查 `Symbol.toStringTag` 属性:
|
||||
|
||||
[![Hydrogen 黑魔法](http://benediktmeurer.de/images/2016/hydrogen-compare-20161216.png)][80]
|
||||
|
||||
@ -392,23 +392,23 @@ $
|
||||
|
||||
![Box2D 加速](http://benediktmeurer.de/images/2016/awfy-box2d-20161216.png)
|
||||
|
||||
我要声明一下,当时我并不相信这个特定的行为总是指向源代码中的漏洞,所以我甚至期望外部代码经常会遇到这种情况,同时也因为我假设 JavaScript 开发人员不会总是关心这些种类的潜在错误。但是,我大错特错了,在此我马上悔改!我不得不承认,这个特殊的优化纯粹是一个基准测试的东西,并不会有助于任何真实代码(除非代码是为了从这个优化中获益而写,不过以后你可以在代码中直接写入 `true` 或 `false`,而不用再总是使用常量关系比较)。你可能想知道我们为什么在打补丁后又马上回滚了一下。这是我们整个团队投入到 ES2015 实施的非常时期,这才是真正的恶魔之舞,我们需要在没有严格的回归测试的情况下将所有新特性(ES2015 就是个怪兽)纳入传统基准。
|
||||
我要声明一下,当时我并不相信这个特定的行为总是指向源代码中的漏洞,所以我甚至期望外部代码经常会遇到这种情况,同时也因为我假设 `JavaScript` 开发人员不会总是关心这些种类的潜在错误。但是,我大错特错了,在此我马上悔改!我不得不承认,这个特殊的优化纯粹是一个基准测试的东西,并不会有助于任何真实代码(除非代码是为了从这个优化中获益而写,不过以后你可以在代码中直接写入 `true` 或 `false`,而不用再总是使用常量关系比较)。你可能想知道我们为什么在打补丁后又马上回滚了一下。这是我们整个团队投入到 `ES2015` 实施的非常时期,这才是真正的恶魔之舞,我们需要在没有严格的回归测试的情况下将所有新特性(`ES2015` 就是个怪兽)纳入传统基准。
|
||||
|
||||
关于 Box2D 点到为止了,让我们看看 Mandreel 基准。Mandreel 是一个用来将 C/C++ 代码编译成 JavaScript 的编译器,它并没有用上新一代的 [Emscripten][82] 编译器所使用,并且已经被弃用(或多或少已经从互联网消失了)大约三年的 JavaScript 子集 [asm.js][81]。然而,Octane 仍然有一个通过 [Mandreel][84] 编译的[子弹物理引擎][83]。MandreelLatency 测试十分有趣,它测试 Mandreel 基准与频繁的时间测量检测点。有一种说法是,由于 Mandreel 强制使用虚拟机编译器,此测试提供了由编译器引入的延迟的指示,并且测量检测点之间的长时间停顿降低了最终得分。这听起来似乎合情合理,确实有一定的意义。然而,像往常一样,供应商找到了在这个基准上作弊的方法。
|
||||
关于 `Box2D` 点到为止了,让我们看看 `Mandreel` 基准。`Mandreel` 是一个用来将 `C/C++` 代码编译成 `JavaScript` 的编译器,它并没有用上新一代的 [Emscripten][82] 编译器所使用,并且已经被弃用(或多或少已经从互联网消失了)大约三年的 `JavaScript` 子集 [asm.js][81]。然而,`Octane` 仍然有一个通过 [Mandreel][84] 编译的[子弹物理引擎][83]。`MandreelLatency` 测试十分有趣,它测试 `Mandreel` 基准与频繁的时间测量检测点。有一种说法是,由于 `Mandreel` 强制使用虚拟机编译器,此测试提供了由编译器引入的延迟的指示,并且测量检测点之间的长时间停顿降低了最终得分。这听起来似乎合情合理,确实有一定的意义。然而,像往常一样,供应商找到了在这个基准上作弊的方法。
|
||||
|
||||
[![Mozilla 1162272 漏洞](http://benediktmeurer.de/images/2016/bugzilla-mandreel-20161216.png)][85]
|
||||
|
||||
Mandreel 自带一个重型初始化函数 `global_init`,光是解析这个函数并为其生成基线代码就花费了不可思议的时间。因为引擎通常在脚本中多次解析各种函数,一个所谓的预解析步骤用来发现脚本内的函数。然后作为函数第一次被调用完整的解析步骤以生成基线代码(或者说字节码)。这在 V8 中被称为[懒解析][86]。V8 有一些启发式检测函数,当预解析浪费时间的时候可以立刻调用,不过对于 Mandreel 基准的 `global_init` 函数就不太清楚了,于是我们将经历这个大家伙“预解析+解析+编译”的长时间停顿。所以我们[添加了一个额外的启发式函数][87]以避免 `global_init` 函数的预解析。
|
||||
`Mandreel` 自带一个重型初始化函数 `global_init`,光是解析这个函数并为其生成基线代码就花费了不可思议的时间。因为引擎通常在脚本中多次解析各种函数,一个所谓的预解析步骤用来发现脚本内的函数。然后作为函数第一次被调用完整的解析步骤以生成基线代码(或者说字节码)。这在 `V8` 中被称为[懒解析][86]。`V8` 有一些启发式检测函数,当预解析浪费时间的时候可以立刻调用,不过对于 `Mandreel` 基准的 `global_init` 函数就不太清楚了,于是我们将经历这个大家伙“预解析+解析+编译”的长时间停顿。所以我们[添加了一个额外的启发式函数][87]以避免 `global_init` 函数的预解析。
|
||||
|
||||
[![MandreelLatency 基准](http://benediktmeurer.de/images/2016/awfy-mandreel-20161216.png)][88]
|
||||
|
||||
由此可见,在检测 `global_init` 和避免昂贵的预解析步骤我们几乎提升了 2 倍。我们不太确定这是否会对真实用例产生负面影响,不过保证你在预解析大函数的时候将会受益匪浅(因为它们不会立即执行)。
|
||||
|
||||
让我们来看看另一个稍有争议的基准测试:[`splay.js`][89] 测试,一个用于处理伸展树(二叉查找树的一种)和练习自动内存管理子系统(也被成为垃圾回收器)的数据操作基准。它自带一个延迟测试,这会引导 Splay 代码通过频繁的测量检测点,检测点之间的长时间停顿表明垃圾回收器的延迟很高。此测试测量延迟暂停的频率,将它们分类到桶中,并以较低的分数惩罚频繁的长暂停。这听起来很棒!没有 GC 停顿,没有垃圾。纸上谈兵到此为止。让我们看看这个基准,以下是整个伸展树业务的核心:
|
||||
让我们来看看另一个稍有争议的基准测试:[`splay.js`][89] 测试,一个用于处理伸展树(二叉查找树的一种)和练习自动内存管理子系统(也被成为垃圾回收器)的数据操作基准。它自带一个延迟测试,这会引导 `Splay` 代码通过频繁的测量检测点,检测点之间的长时间停顿表明垃圾回收器的延迟很高。此测试测量延迟暂停的频率,将它们分类到桶中,并以较低的分数惩罚频繁的长暂停。这听起来很棒!没有 `GC` 停顿,没有垃圾。纸上谈兵到此为止。让我们看看这个基准,以下是整个伸展树业务的核心:
|
||||
|
||||
[![splay.js](http://benediktmeurer.de/images/2016/splay-insertnode-20161216.png)][90]
|
||||
|
||||
这是伸展树结构的核心,尽管你可能想看完整的基准,不过这或多或少是 SplayLatency 得分的重要来源。怎么回事?实际上,基准测试是建立巨大的伸展树,尽可能保留所有节点,从而还原它原本的空间。使用像 V8 这样的代数垃圾回收器,如果程序违反了[代数假设][91],导致极端的时间停顿,从本质上看,将所有东西从新空间撤回到旧空间的开销是非常昂贵的。在旧配置中运行 V8 可以清楚地展示这个问题:
|
||||
这是伸展树结构的核心,尽管你可能想看完整的基准,不过这或多或少是 `SplayLatency` 得分的重要来源。怎么回事?实际上,基准测试是建立巨大的伸展树,尽可能保留所有节点,从而还原它原本的空间。使用像 `V8` 这样的代数垃圾回收器,如果程序违反了[代数假设][91],导致极端的时间停顿,从本质上看,将所有东西从新空间撤回到旧空间的开销是非常昂贵的。在旧配置中运行 `V8` 可以清楚地展示这个问题:
|
||||
|
||||
```
|
||||
$ out/Release/d8 --trace-gc --noallocation_site_pretenuring octane-splay.js
|
||||
@ -491,7 +491,7 @@ $ out/Release/d8 --trace-gc --noallocation_site_pretenuring octane-splay.js
|
||||
$
|
||||
```
|
||||
|
||||
因此这里关键的发现是直接在旧空间中分配伸展树节点可基本避免在周围复制对象的所有开销,并且将次要 GC 周期的数量减少到最小(从而减少 GC 引起的停顿时间)。我们想出了一种称为[分配站点预占][92]的机制,当运行到基线代码时,将尝试动态收集分配站点的反馈,以决定在此分配的对象的部分是否确切存在,如果是,则优化代码以直接在旧空间分配对象——即预占对象。
|
||||
因此这里关键的发现是直接在旧空间中分配伸展树节点可基本避免在周围复制对象的所有开销,并且将次要 `GC` 周期的数量减少到最小(从而减少 `GC` 引起的停顿时间)。我们想出了一种称为[分配站点预占][92]的机制,当运行到基线代码时,将尝试动态收集分配站点的反馈,以决定在此分配的对象的部分是否确切存在,如果是,则优化代码以直接在旧空间分配对象——即预占对象。
|
||||
|
||||
```
|
||||
$ out/Release/d8 --trace-gc octane-splay.js
|
||||
@ -518,15 +518,15 @@ $ out/Release/d8 --trace-gc octane-splay.js
|
||||
$
|
||||
```
|
||||
|
||||
事实上,这完全解决了 SplayLatency 基准的问题,并将我们的得分超过 250%!
|
||||
事实上,这完全解决了 `SplayLatency` 基准的问题,并提高我们的得分至超过 250%!
|
||||
|
||||
[![SplayLatency 基准](http://benediktmeurer.de/images/2016/awfy-splay-20161216.png)][93]
|
||||
|
||||
正如 [SIGPLAN 论文][94] 中所提及的,我们有充分的理由相信,分配站点预占机制可能真的赢得了真实世界应用的欢心,并真正期待看到改进和扩展后的机制,那时将不仅仅是对象和数组字面量。但是不久后我们意识到[分配站点预占机制对真实世界引用产生了相当严重的负面影响][97]。我们实际上得到很多负面新闻,包括 Ember.js 开发者和用户的唇枪舌战,不仅是因为分配站点预占机制,不过它是事故的罪魁祸首。
|
||||
正如 [SIGPLAN 论文][94] 中所提及的,我们有充分的理由相信,分配站点预占机制可能真的赢得了真实世界应用的欢心,并真正期待看到改进和扩展后的机制,那时将不仅仅是对象和数组字面量。但是不久后我们意识到[分配站点预占机制对真实世界引用产生了相当严重的负面影响][97]。我们实际上得到很多负面新闻,包括 `Ember.js` 开发者和用户的唇枪舌战,不仅是因为分配站点预占机制,不过它是事故的罪魁祸首。
|
||||
|
||||
分配站点预占机制的基本问题数之不尽,这在今天的应用中非常常见(主要是由于框架,同时还有其它原因),假设你的对象工厂最初是用于创建构成你的对象模型和视图的长周期对象的,它将你的工厂方法中的分配站点转换为永久状态,并且从工厂分配的所有内容都立即转到旧空间。现在初始设置完成后,你的应用开始工作,作为其中的一部分,从工厂分配临时对象会污染旧空间,最终导致开销昂贵的垃圾回收周期以及其它负面的副作用,例如过早触发增量标记。
|
||||
|
||||
我们开始重新考虑基准驱动的努力,并开始寻找驱动真实世界的替代方案,这导致了 [Orinoco][98] 的诞生,它的目标是逐步改进垃圾回收器;这个努力的一部分是一个称为“统一堆”的项目,如果页面中的所有内容都存在,它将尝试避免复制对象。也就是说站在更高的层面看:如果新空间充满活动对象,只需将所有新空间页面标记为属于旧空间,然后从空白页面创建一个新空间。这可能不会在 SplayLatency 基准测试中得到相同的分数,但是这对于真实世界的用例更友好,它可以自动适配具体的用例。我们还考虑并发标记,将标记工作卸载到单独的线程,从而进一步减少增量标记对延迟和吞吐量的负面影响。
|
||||
我们开始重新考虑基准驱动的工作,并开始寻找驱动现实场景的替代方案,这导致了 [Orinoco][98] 的诞生,它的目标是逐步改进垃圾回收器;这个努力的一部分是一个称为“统一堆”的项目,如果页面中的所有内容都存在,它将尝试避免复制对象。也就是说站在更高的层面看:如果新空间充满活动对象,只需将所有新空间页面标记为属于旧空间,然后从空白页面创建一个新空间。这可能不会在 `SplayLatency` 基准测试中得到相同的分数,但是这对于真实用例更友好,它可以自动适配具体的用例。我们还考虑并发标记,将标记工作卸载到单独的线程,从而进一步减少增量标记对延迟和吞吐量的负面影响。
|
||||
|
||||
### 轻松一刻
|
||||
|
||||
@ -534,7 +534,7 @@ $
|
||||
|
||||
喘口气。
|
||||
|
||||
好吧,我想这对于我的观点的强调应该足够了。我可以继续指出更多的例子,其中 Octane 驱动的改进后来变成了一个坏主意,也许改天我会接着写下去。但是今天就到此为止了吧。
|
||||
好吧,我想这足以强调我的观点了。我可以继续指出更多的例子,其中 `Octane` 驱动的改进后来变成了一个坏主意,也许改天我会接着写下去。但是今天就到此为止了吧。
|
||||
|
||||
### 结论
|
||||
|
||||
@ -542,11 +542,11 @@ $
|
||||
|
||||
[![2016 年 10 月浏览器基准之战: Chrome、Firefox 和 Edge 的决战](http://benediktmeurer.de/images/2016/venturebeat-20161216.png)][99]
|
||||
|
||||
没人害怕竞争,但是玩弄可能已经坏掉的基准不像是在合理使用工程时间。我们可以尽更大的努力,并把 JavaScript 提高到更高的水平。让我们开展有意义的性能测试,以便为最终用户和开发者带来有意思的领域竞争。此外,让我们再对服务器和运行在 Node.js(还有 V8 和 ChakraCore)中的工具代码做一些有意义的改进!
|
||||
没人害怕竞争,但是玩弄可能已经坏掉的基准不像是在合理使用工程时间。我们可以尽更大的努力,并把 JavaScript 提高到更高的水平。让我们开展有意义的性能测试,以便为最终用户和开发者带来有意思的领域竞争。此外,让我们再对服务器和运行在 `Node.js`(还有 `V8` 和 `ChakraCore`)中的工具代码做一些有意义的改进!
|
||||
|
||||
![](http://benediktmeurer.de/images/2016/measure-20161216.jpg)
|
||||
|
||||
结束语:不要用传统的 JavaScript 基准来比较手机。这是真正最没用的事情,因为 JavaScript 的性能通常取决于软件,而不一定是硬件,并且 Chrome 每 6 周发布一个新版本,所以你在三月的测试结果到了四月就已经毫不相关了。如果在浏览器中发送一个数字都一部手机不可避免,那么至少请使用一个现代健全的浏览器基准来测试,至少这个基准要知道人们会用浏览器来干什么,比如 [Speedometer 基准][100]。
|
||||
结束语:不要用传统的 `JavaScript` 基准来比较手机。这是真正最没用的事情,因为 `JavaScript` 的性能通常取决于软件,而不一定是硬件,并且 `Chrome` 每 6 周发布一个新版本,所以你在三月的测试结果到了四月就已经毫不相关了。如果在浏览器中发送一个数字都一部手机不可避免,那么至少请使用一个现代健全的浏览器基准来测试,至少这个基准要知道人们会用浏览器来干什么,比如 [Speedometer 基准][100]。
|
||||
|
||||
感谢你花时间阅读!
|
||||
|
||||
@ -563,8 +563,8 @@ $
|
||||
via: http://benediktmeurer.de/2016/12/16/the-truth-about-traditional-javascript-benchmarks
|
||||
|
||||
作者:[Benedikt Meurer][a]
|
||||
译者:[译者ID](https://github.com/OneNewLife)
|
||||
校对:[校对者ID](https://github.com/校对者ID)
|
||||
译者:[OneNewLife](https://github.com/OneNewLife)
|
||||
校对:[OneNewLife](https://github.com/OneNewLife)
|
||||
|
||||
本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译,[Linux中国](https://linux.cn/) 荣誉推出
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user