引言
这篇文档包含了如何避免使代码性能远低于预期的建议. 尤其是一些会导致 V8 (牵涉到 Node.js, Opera, Chromium 等) 无法优化相关函数的问题.
一些 V8 背景
在 V8 中并没有解释器, 但却有两个不同的编译器: 通用编译器和优化编译器. 这意味着你的 JavaScript 代码总是会被编译为机器码后直接运行. 这样一定很快咯? 并不是. 仅仅是编译为本地代码并不能明显提高性能. 它只是消除了解释器的开销, 但如果未被优化, 代码依旧很慢.
举个例子, 使用通用编译器, a + b
会变成这个样子:
1 2 3 |
mov eax, a mov ebx, b call RuntimeAdd |
换言之它仅仅是调用了运行时的函数. 如果 a
和 b
一定是整数, 那可以像这样:
1 2 3 |
mov eax, a mov ebx, b add eax, ebx |
相比而言这会远快于调用需要处理复杂 JavaScript 运行时语义的函数.
通常来说, 通用编译器得到的是第一种结果, 而优化编译器则会得到第二种结果. 使用优化编译器编译的代码可以很容易比通用编译器编译的代码快上 100 倍. 但这里有个坑, 并非所有的 JavaScript 代码都能被优化. 在 JavaScript 中有很多种写法, 包括具备语义的, 都不能被优化编译器编译 (回落到通用编译器*).
记下一些会导致整个函数无法使用优化编译器的用法很重要. 一次代码优化的是一整个函数, 优化过程中并不会关心其他代码做了什么 (除非代码在已经被优化的函数中).
这个指南会涵盖多数会导致整个函数掉进 “反优化火狱” 的例子. 由于编译器一直在不断更新, 未来当它能够识别下面的一些情况时, 这里提到的处理方法可能也就不必要了.
1. 工具和方法
你可以通过添加一些 V8 标记来使用 Node.js 验证不同的用法如何影响优化结果. 通常可以写一个包含了特定用法的函数, 使用所有可能的参数类型去调用它, 再使用 V8 的内部函数去优化和审查.
test.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
// 包含需要审查的用法的函数 (这里是 with 语句) function containsWith() { return 3; with({}) { } } function printStatus(fn) { switch(%GetOptimizationStatus(fn)) { case 1: console.log("Function is optimized"); break; case 2: console.log("Function is not optimized"); break; case 3: console.log("Function is always optimized"); break; case 4: console.log("Function is never optimized"); break; case 6: console.log("Function is maybe deoptimized"); break; } } // 告诉编译器类型信息 containsWith(); // 为了使状态从 uninitialized 变为 pre-monomorphic, 再变为 monomorphic, 两次调用是必要的 containsWith(); %OptimizeFunctionOnNextCall(containsWith); // 下一次调用 containsWith(); // 检查 printStatus(containsWith); |
执行:
1 2 |
$ node --trace_opt --trace_deopt --allow-natives-syntax test.js Function is not optimized |
作为是否被优化的对比, 注释掉 with
语句再来一次:
1 2 3 |
$ node --trace_opt --trace_deopt --allow-natives-syntax test.js [optimizing 000003FFCBF74231 <JS Function containsWith (SharedFunctionInfo 00000000FE1389E1)> - took 0.345, 0.042, 0.010 ms] Function is optimized |
使用这个方法来验证处理方法有效且必要是很重要的.
2. 不支持的语法
优化编译器不支持一些特定的语句, 使用这些语法会使包含它的函数无法得到优化.
有一点请注意, 即使这些语句无法到达或者不会被执行, 它们也会使相关函数无法被优化.
比如这样做是没用的:
1 2 3 |
if (DEVELOPMENT) { debugger; } |
上面的代码会导致包含它的整个函数不被优化, 即使从来不会执行到 debugger 语句.
目前不会被优化的有:
- generator 函数
- 包含 for…of 语句的函数
- 包含 try…catch 的函数
- 包含 try…finally 的函数
- 包含复合
let
赋值语句的函数 (原文为 compoundlet
assignment) - 包含复合
const
赋值语句的函数 (原文为 compoundconst
assignment) - 包含含有 __proto__ 或者 get/set 声明的对象字面量的函数
可能永远不会被优化的有:
- 包含
debugger
语句的函数 - 包含字面调用
evode> 赋值语句的函数 (原文为 compound
let
assignment) - 包含复合
const
赋值语句的函数 (原文为 compoundconst
assignment) - 包含含有 __proto__ 或者 get/set 声明的对象字面量的函数
可能永远不会被优化的有:
- 包含
debugger
语句的函数 - 包含字面调用
ev V8 中并没有解释器, 但却有两个不同的编译器: 通用编译器和优化编译器. 这意味着你的 JavaScript 代码总是会被编译为机器码后直接运行. 这样一定很快咯? 并不是. 仅仅是编译为本地代码并不能明显提高性能. 它只是消除了解释器的开销, 但如果未被优化, 代码依旧很慢.
举个例子, 使用通用编译器,
a + b
会变成这个样子:123mov eax, amov ebx, bcall RuntimeAdd换言之它仅仅是调用了运行时的函数. 如果
a
和b
一定是整数, 那可以像这样:123mov eax, amov ebx, badd eax, ebx相比而言这会远快于调用需要处理复杂 JavaScript 运行时语义的函数.
通常来说, 通用编译器得到的是第一种结果, 而优化编译器则会得到第二种结果. 使用优化编译器编译的代码可以很容易比通用编译器编译的代码快上 100 倍. 但这里有个坑, 并非所有的 JavaScript 代码都能被优化. 在 JavaScript 中有很多种写法, 包括具备语义的, 都不能被优化编译器编译 (回落到通用编译器*).
记下一些会导致整个函数无法使用优化编译器的用法很重要. 一次代码优化的是一整个函数, 优化过程中并不会关心其他代码做了什么 (除非代码在已经被优化的函数中).
这个指南会涵盖多数会导致整个函数掉进 “反优化火狱” 的例子. 由于编译器一直在不断更新, 未来当它能够识别下面的一些情况时, 这里提到的处理方法可能也就不必要了.
1. 工具和方法
你可以通过添加一些 V8 标记来使用 Node.js 验证不同的用法如何影响优化结果. 通常可以写一个包含了特定用法的函数, 使用所有可能的参数类型去调用它, 再使用 V8 的内部函数去优化和审查.
test.js
12345678910111213141516171819202122232425262728// 包含需要审查的用法的函数 (这里是 with 语句)function containsWith() {return 3;with({}) { }}function printStatus(fn) {switch(%GetOptimizationStatus(fn)) {case 1: console.log("Function is optimized"); break;case 2: console.log("Function is not optimized"); break;case 3: console.log("Function is always optimized"); break;case 4: console.log("Function is never optimized"); break;case 6: console.log("Function is maybe deoptimized"); break;}}// 告诉编译器类型信息containsWith();// 为了使状态从 uninitialized 变为 pre-monomorphic, 再变为 monomorphic, 两次调用是必要的containsWith();%OptimizeFunctionOnNextCall(containsWith);// 下一次调用containsWith();// 检查printStatus(containsWith);执行:
12$ node --trace_opt --trace_deopt --allow-natives-syntax test.jsFunction is not optimized作为是否被优化的对比, 注释掉
with
语句再来一次:123$ node --trace_opt --trace_deopt --allow-natives-syntax test.js[optimizing 000003FFCBF74231 <JS Function containsWith (SharedFunctionInfo 00000000FE1389E1)> - took 0.345, 0.042, 0.010 ms]Function is optimized使用这个方法来验证处理方法有效且必要是很重要的.
2. 不支持的语法
优化编译器不支持一些特定的语句, 使用这些语法会使包含它的函数无法得到优化.
有一点请注意, 即使这些语句无法到达或者不会被执行, 它们也会使相关函数无法被优化.
比如这样做是没用的:
123if (DEVELOPMENT) {debugger;