From 9e1e0886606b121362dd1791009c4149035e21aa Mon Sep 17 00:00:00 2001 From: wxy Date: Mon, 11 Sep 2017 17:56:28 +0800 Subject: [PATCH] =?UTF-8?q?=E6=A0=A1=E5=AF=B9=E9=83=A8=E5=88=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...about traditional JavaScript benchmarks.md | 84 ++++++++++--------- 1 file changed, 44 insertions(+), 40 deletions(-) diff --git a/translated/tech/20161216 The truth about traditional JavaScript benchmarks.md b/translated/tech/20161216 The truth about traditional JavaScript benchmarks.md index 3786efbeeb..640726a86e 100644 --- a/translated/tech/20161216 The truth about traditional JavaScript benchmarks.md +++ b/translated/tech/20161216 The truth about traditional JavaScript benchmarks.md @@ -33,17 +33,17 @@ ### 臭名昭著的 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 的时间内完成这个测试。然而,这种优化将局限于这种特殊情况,并且测试的轻微修改可能不再触发它。 +有一些算法需要进行快速的 AND 位运算,特别是从 `C/C++` 转译成 JavaScript 的地方,所以快速执行该操作确实有点意义。然而,现实场景中的网页可能不关心引擎在循环中执行 AND 位运算是否比另一个引擎快两倍。但是再盯着这段代码几秒钟后,你可能会注意到在第一次循环迭代之后 `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` 属性的对象组成的数组: ``` ([ @@ -83,7 +83,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 @@ -99,7 +99,7 @@ } ``` -事实上,将基准测试更新到现代 JavaScript 会立刻提升性能,正如今天的 `V8 LKGR` 从 36ms 降到了 26ms,性能足足提升了 30%! +事实上,将基准测试更新到现代 JavaScript 会立刻会性能暴增,正如今天的 `V8 LKGR` 从 36ms 降到了 26ms,性能足足提升了 30%! ``` $ node string-tagcloud.js.ORIG @@ -111,9 +111,9 @@ 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] @@ -121,55 +121,59 @@ $ [![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.017453292519943295 +* 0.05235987755982989 * 0.08726646259971647 -显然,你可以在这里做的一件事情就是通过缓存以前的计算值来避免重复计算相同的正弦值和余弦值。事实上,这是 V8 以前的做法,而其它引擎例如 `SpiderMonkey` 仍然这样做。我们从 V8 中删除了所谓的超载缓存,因为缓存的开销在现实中的工作负载是不可忽视的,你不可能总是在一行代码中计算相同的值,这在其它地方倒不稀奇。当我们在 2013 和 2014 年移除这个特定的基准优化时,我们对 SunSpider 基准产生了强烈的冲击,但我们完全相信,优化基准并没有任何意义,同时以这种方式批判现实场景中的使用案例。 +显然,你可以在这里做的一件事情就是通过缓存以前的计算值来避免重复计算相同的正弦值和余弦值。事实上,这是 V8 以前的做法,而其它引擎例如 `SpiderMonkey` 目前仍然在这样做。我们从 V8 中删除了所谓的超载缓存transcendental cache,因为缓存的开销在实际的工作负载中是不可忽视的,你不可能总是在一行代码中计算相同的值,这在其它地方倒不稀奇。当我们在 2013 和 2014 年移除这个特定的基准优化时,我们对 SunSpider 基准产生了强烈的冲击,但我们完全相信,为基准而优化并没有任何意义,并同时以这种方式批判了现实场景中的使用案例。 [![3d-cube 基准](http://benediktmeurer.de/images/2016/3d-cube-awfy-20161216.png)][52] -显然,处理恒定正弦/余弦输入的更好的方法是一个内联的启发式算法,它试图平衡内联因素与其它不同的因素,例如在调用位置优先选择内联,其中恒定折叠可以是有益的,例如在 `RotateX`、`RotateY` 和 `RotateZ` 中调用边界值的案例。但是出于各种原因,这对于 `Crankshaft` 编译器并不可行。使用 `Ignition` 和 `TurboFan` 倒是一个明智的选择,我们已经在开发更好的[内联启发式算法][53]。 +(来源:[arewefastyet.com](https://arewefastyet.com/#machine=12&view=single&suite=ss&subtest=cube&start=1343350217&end=1415382608)) -### 垃圾回收是有害的 +显然,处理恒定正弦/余弦输入的更好的方法是一个内联的启发式算法,它试图平衡内联因素与其它不同的因素,例如在调用位置优先选择内联,其中常量叠算constant folding可以是有益的,例如在 `RotateX`、`RotateY` 和 `RotateZ` 调用位置的案例中。但是出于各种原因,这对于 `Crankshaft` 编译器并不可行。使用 `Ignition` 和 `TurboFan` 倒是一个明智的选择,我们已经在开发更好的[内联启发式算法][53]。 -除了这些非常具体的测试问题,SunSpider 还有一个根本的问题:总体执行时间。目前 V8 在体面的英特尔硬件上运行整个基准测试大概只需要 200ms(使用默认配置)。次要的 `GC` 在 1ms 到 25ms 之间(取决于新空间中的活对象和旧空间的碎片),而主 `GC` 暂停可以浪费掉 30ms(甚至不考虑增量标记的开销),这超过了 SunSpider 套件总体执行时间的 10%!因此,任何不想因 `GC` 循环而造成减速 10-20% 的引擎,必须用某种方式确保它在运行 SunSpider 时不会触发 `GC`。 +#### 垃圾回收(GC)是有害的 + +除了这些非常具体的测试问题,SunSpider 基准测试还有一个根本性的问题:总体执行时间。目前 V8 在适当的英特尔硬件上运行整个基准测试大概只需要 200ms(使用默认配置)。次垃圾回收minor GC在 1ms 到 25ms 之间(取决于新空间中的存活对象和旧空间的碎片),而主垃圾回收major GC暂停的话可以轻松减掉 30ms(甚至不考虑增量标记的开销),这超过了 SunSpider 套件总体执行时间的 10%!因此,任何不想因垃圾回收循环而造成减速 10-20% 的引擎,必须用某种方式确保它在运行 SunSpider 时不会触发垃圾回收。 [![driver-TEMPLATE.html](http://benediktmeurer.de/images/2016/sunspider-driver-20161216.png)][54] -就实现而言,有不同的方案,不过就我所知,没有一个在现实场景中产生了任何积极的影响。V8 使用了一个相当简单的技巧:由于每个 SunSpider 套件都运行在一个新的 `