摘要:原文引言這篇文檔包含了如何避免使代碼性能遠低于預期的建議尤其是一些會導致牽涉到等無法優化相關函數的問題一些背景在中并沒有解釋器但卻有兩個不同的編譯器通用編譯器和優化編譯器這意味著你的代碼總是會被編譯為機器碼后直接運行這樣一定很快咯并不是
原文:http://dev.zm1v1.com/2015/08/19/javascript-optimization-killers/
引言
這篇文檔包含了如何避免使代碼性能遠低于預期的建議. 尤其是一些會導致 V8 (牽涉到 Node.js, Opera, Chromium 等) 無法優化相關函數的問題.
一些 V8 背景
在 V8 中并沒有解釋器, 但卻有兩個不同的編譯器: 通用編譯器和優化編譯器. 這意味著你的 JavaScript 代碼總是會被編譯為機器碼后直接運行. 這樣一定很快咯? 并不是. 僅僅是編譯為本地代碼并不能明顯提高性能. 它只是消除了解釋器的開銷, 但如果未被優化, 代碼依舊很慢.
舉個例子, 使用通用編譯器, a + b 會變成這個樣子:
mov eax, a
mov ebx, b
call RuntimeAdd
換言之它僅僅是調用了運行時的函數. 如果 a 和 b 一定是整數, 那可以像這樣:
mov eax, a
mov ebx, b
add eax, ebx
相比而言這會遠快于調用需要處理復雜 JavaScript 運行時語義的函數.
通常來說, 通用編譯器得到的是第一種結果, 而優化編譯器則會得到第二種結果. 使用優化編譯器編譯的代碼可以很容易比通用編譯器編譯的代碼快上 100 倍. 但這里有個坑, 并非所有的 JavaScript 代碼都能被優化. 在 JavaScript 中有很多種寫法, 包括具備語義的, 都不能被優化編譯器編譯 (回落到通用編譯器*).
記下一些會導致整個函數無法使用優化編譯器的用法很重要. 一次代碼優化的是一整個函數, 優化過程中并不會關心其他代碼做了什么 (除非代碼在已經被優化的函數中).
這個指南會涵蓋多數會導致整個函數掉進 “反優化火獄” 的例子. 由于編譯器一直在不斷更新, 未來當它能夠識別下面的一些情況時, 這里提到的處理方法可能也就不必要了.
索引
工具和方法
不支持的語法
使用 arguments
switch…case
for…in
無限循環
工具和方法
你可以通過添加一些 V8 標記來使用 Node.js 驗證不同的用法如何影響優化結果. 通??梢詫懸粋€包含了特定用法的函數, 使用所有可能的參數類型去調用它, 再使用 V8 的內部函數去優化和審查.
test.js
// 包含需要審查的用法的函數 (這里是 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);
執行:
$ node --trace_opt --trace_deopt --allow-natives-syntax test.js
Function is not optimized
作為是否被優化的對比, 注釋掉 with 語句再來一次:
$ node --trace_opt --trace_deopt --allow-natives-syntax test.js
[optimizing 000003FFCBF74231
Function is optimized
使用這個方法來驗證處理方法有效且必要是很重要的.
不支持的語法
優化編譯器不支持一些特定的語句, 使用這些語法會使包含它的函數無法得到優化.
有一點請注意, 即使這些語句無法到達或者不會被執行, 它們也會使相關函數無法被優化.
比如這樣做是沒用的:
if (DEVELOPMENT) {
debugger;
}
上面的代碼會導致包含它的整個函數不被優化, 即使從來不會執行到 debugger 語句.
目前不會被優化的有:
generator 函數
包含 for…of 語句的函數
包含 try…catch 的函數
包含 try…finally 的函數
包含復合 let 賦值語句的函數 (原文為 compound let assignment)
包含復合 const 賦值語句的函數 (原文為 compound const assignment)
包含含有 proto 或者 get/set 聲明的對象字面量的函數
可能永遠不會被優化的有:
包含 debugger 語句的函數
包含字面調用 eval() 的函數
包含 with 語句的函數
最后一點明確一下, 如果有下面任何的情況, 整個函數都無法被優化:
function containsObjectLiteralWithProto() {
return { __proto__: 3 };
}
function containsObjectLiteralWithGetter() {
return { get prop() { return 3; } };
}
function containsObjectLiteralWithSetter() {
return { set prop(val) { this.val = val; } };
}
提一下直接使用 eval 和 with 的情況, 因為它們會造成相關嵌套的函數作用域變為動態的. 這樣一來則有可能也影響其他很多函數, 因為這種情況下無法從詞法上判斷相關變量的有效范圍.
處理方法
之前提到過的一些語句在生產環境中是無法避免的, 比如 try...finally 和 try...catch. 為了是代價最小, 它們必須被隔離到一個最小化的函數, 以保證主要的代碼不受影響.
var errorObject = { value: null };
function tryCatch(fn, ctx, args) {
try { return fn.apply(ctx, args); } catch(e) { errorObject.value = e; return errorObject; }
}
var result = tryCatch(mightThrow, void 0, [1,2,3]);
// 不帶歧義地判斷是否調用拋出了異常 (或其他值)
if(result === errorObject) {
var error = errorObject.value;
} else {
// 結果是返回值
}
使用 arguments
有不少使用 arguments 的方式會導致相關函數無法被優化. 所以在使用 arguments 的時候需要非常留意.
3.1. 給一個已經定義的參數重新賦值, 并且在相關語句主體中引用 (僅限非嚴格模式). 典型的例子:
function defaultArgsReassign(a, b) {
if (arguments.length < 2) b = 5;
}
處理方法則是賦值該參數給一個新的變量:
function reAssignParam(a, b_) {
var b = b_; // 與 b_ 不同, b 可以安全地被重新賦值 if (arguments.length < 2) b = 5;
}
如果僅僅是在這種情況下在函數中用到了 arguments, 也可以寫為是否為 undefined 的判斷:
function reAssignParam(a, b) {
if (b === void 0) b = 5;
}
然而如果之后這個函數中用到 arguments, 維護代碼的同學可能會容易忘掉要把重新賦值的語句留下**.
第二個處理方法: 對整個文件或者函數開啟嚴格模式 ("use strict").
3.2. 泄露 arguments:
function leaksArguments1() {
return arguments;
}
function leaksArguments2() {
var args = [].slice.call(arguments);
}
function leaksArguments3() {
var a = arguments; return function() { return a; };
}
arguments 對象不能被傳遞或者泄露到任何地方.
處理方法則是使用內聯的代碼創建數組:
function doesntLeakArguments() {
// .length 只是一個整數, 它不會泄露 // arguments 對象本身 var args = new Array(arguments.length); for(var i = 0; i < args.length; ++i) { // i 始終是 arguments 對象的有效索引 args[i] = arguments[i]; } return args;
}
寫一堆代碼很讓人惱火, 所以分析是否值得這么做是值得的. 接下來更多的優化總是會帶來更多的代碼, 而更多的代碼又意味著語義上更顯而易見的退化.
然而如果你有一個 build 的過程, 這其實可以被一個不必要求 source map 的宏來實現, 同時保證源代碼是有效的 JavaScript 代碼.
function doesntLeakArguments() {
INLINE_SLICE(args, arguments); return args;
}
上面的技巧就用到了 Bluebird 中, 在 build 后會被擴充為下面這樣:
function doesntLeakArguments() {
var $_len = arguments.length;var args = new Array($_len); for(var $_i = 0; $_i < $_len; ++$_i) {args[$_i] = arguments[$_i];} return args;
}
3.3. 對 arguments 賦值
在非嚴格模式下, 這其實是可能的:
function assignToArguments() {
arguments = 3; return arguments;
}
處理方法: 沒必要寫這么蠢的代碼. 說來在嚴格模式下, 它也會直接拋出異常.
怎樣安全地使用 arguments?
僅使用:
arguments.length
arguments[i] 這里 i 必須一直是 arguments 的整數索引, 并且不能超出邊界
除了 .length 和 [i], 永遠不要直接使用 arguments (嚴格地說 x.apply(y, arguments) 是可以的, 但其他的都不行, 比如 .slice. Function#apply 比較特殊)
另外關于用到 arguments 會造成 arguments 對象的分配這一點的 FUD (恐懼), 在使用限于上面提到的安全的方式時是不必要的.
switch…case
一個 switch…case 語句目前可以有最多 128 個 case 從句, 如果超過了這個數量, 包含這個 switch 語句的函數就無法被優化.
function over128Cases(c) {
switch(c) { case 1: break; case 2: break; case 3: break; ... case 128: break; case 129: break; }
}
所以請保證 switch 語句的 case 從句不超過 128 個, 可以使用函數數組或者 if…else 代替.
for…in
for…in 語句在一些情況下可能導致包含它的函數無法被優化.
以下解釋了 “for…in 不快” 或者類似的原因.
鍵不是局部變量:
function nonLocalKey1() {
var obj = {} for(var key in obj); return function() { return key; };
}
var key;
function nonLocalKey2() {
var obj = {} for(key in obj);
}
因此鍵既不能是上級作用于的變量, 也不能被子作用域引用. 它必須是一個本地變量.
5.2. 被枚舉的對象不是一個 “簡單的可枚舉對象”
5.2.1. 處于 “哈希表模式” 的對象 (即 “普通化的對象”, “字典模式” – 以哈希表為數據輔助結構的對象) 不是簡單的可枚舉對象.
function hashTableIteration() {
var hashTable = {"-": 3}; for(var key in hashTable);
}
如果你 (在構造函數外) 動態地添加太多屬性到一個對象, 刪除屬性, 使用不是合法標識符 (identifier) 的屬性名稱, 這個對象就會變為哈希表模式. 換言之, 如果你把一個對象當做哈希表來使用, 它就會轉變為一個哈希表. 不要再 for…in 中使用這樣的對象. 判斷一個對象是否為哈希表模式, 可以在開啟 Node.js 的 --allow-natives-syntax 選項時調用 console.log(%HasFastProperties(obj)).
5.2.2. 對象的原型鏈中有可枚舉的屬性
Object.prototype.fn = function() {};
添加上面的代碼會使所有的對象 (除了 Object.create(null) 創建的對象) 的原型鏈中都存在一個可枚舉的屬性. 由此任何包含 for…in 語句的函數都無法得到優化 (除非僅枚舉 Object.create(null) 創建的對象).
你可以通過 Object.defineProperty 來創建不可枚舉的屬性 (不推薦運行時調用, 但是高效地定義一些靜態的東西, 比如原型屬性, 還是可以的).
5.2.3. 對象包含可枚舉的數組索引
一個屬性是否是數組索引是在 ECMAScript 規范 中定義的.
A property name P (in the form of a String value) is an array index if and only if ToString(ToUint32(P)) is equal to P and ToUint32(P) is not equal to 232?1. A property whose property name is an array index is also called an element
通常來說這些對象是數組, 但普通的對象也可以有數組索引: normalObj[0] = value;
function iteratesOverArray() {
var arr = [1, 2, 3]; for (var index in arr) { }
}
所以使用 for…in 遍歷數組不僅比 for 循環慢, 還會導致包含它的整個函數無法被優化.
如果傳遞一個非簡單的可枚舉對象到 for…in, 會導致整個函數無法被優化.
處理方法: 總是使用 Object.keys 再使用 for 循環遍歷數組. 如果的確需要原型鏈上的所有屬性, 創建一個多帶帶的輔助函數.
function inheritedKeys(obj) {
var ret = []; for(var key in obj) { ret.push(key); } return ret;
}
退出條件較深或者不明確的無限循環
寫代碼的時候, 有時會知道自己需要一個循環, 但不清楚循環內的代碼會寫成什么樣子. 所以你放了一個 while (true) { 或者 for (;;) {, 之后再在一定條件下中斷循環接續之后的代碼, 最后忘了這么一件事. 重構的時間到了, 你發現這個函數很慢, 或者發現一個反優化的情況 – 可能它就是罪魁.
將循環的退出條件重構到循環自己的條件部分可能并不容易. 如果代碼的退出條件是結尾 if 語句的一部分, 并且代碼至少會執行一次, 那可以重構為 do { } while (); 循環. 如果退出條件在循環開頭, 把它放進循環本身的條件部分. 如果退出條件在中間, 你可以嘗試 “滾動” 代碼: 每每從開頭移動一部分代碼到末尾, 也復制一份到循環開始之前. 一旦退出條件可以放置在循環的條件部分, 或者至少是一個比較淺的邏輯判斷, 這個循環應該就不會被反優化了.
原文 it “bails out”.
** 原文 maintenance could easily forget to leave the re-assignent there though.
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/86007.html
摘要:譯文地址譯唯快不破應用的個優化步驟前端的逆襲知乎專欄原文地址時過境遷,應用比以往任何時候都更具交互性。使用負載均衡方案我們在之前討論緩存的時候簡要提到了內容分發網絡。換句話說,元素的串形訪問會削弱負載均衡器以最佳形式 歡迎關注知乎專欄 —— 前端的逆襲歡迎關注我的博客,知乎,GitHub。 譯文地址:【譯】唯快不破:Web 應用的 13 個優化步驟 - 前端的逆襲 - 知乎專欄原文地...
摘要:前端日報精選一起探索的眾成翻譯性能優化殺手掘金入門知乎專欄用實現無限循環的無縫滾動蚊子的博客前端每周清單組件解耦之道基于的自動化測試是否為時已晚中文譯如何在無損的情況下讓圖片變的更小掘金第期用上古思想寫現代前端踩坑集錦掘金 2017-07-04 前端日報 精選 一起探索 ES6 的 Generators - 眾成翻譯V8 性能優化殺手 - 掘金入門TypeScript React - ...
摘要:在當前階段,僅僅只是字節碼規范。如果都沒有將代碼編譯為字節碼的工具,要起步就很困難了。接下來要做的是使用將格式的代碼轉換為二進制碼。運行文件,最后就能得到瀏覽器需要的真正的二進制碼。 本文轉載自:眾成翻譯譯者:文藺鏈接:http://www.zcfy.cc/article/1031原文:http://cultureofdevelopment.com/blog/build-your-fi...
摘要:在年成為最大贏家,贏得了實現的風暴之戰。和他的競爭者位列第二沒有前端開發者可以忽視和它的生態系統。他的殺手級特性是探測功能,通過檢查任何用戶的功能,以直觀的方式讓開發人員檢查所有端點。 2016 JavaScript 后起之秀 本文轉載自:眾成翻譯譯者:zxhycxq鏈接:http://www.zcfy.cc/article/2410原文:https://risingstars2016...
摘要:文章的第二部分涵蓋了內存管理的概念,不久后將發布。的標準化工作是由國際組織負責的,相關規范被稱為或者。隨著分析器和編譯器不斷地更改字節碼,的執行性能逐漸提高。 原文地址:How Does JavaScript Really Work? (Part 1) 原文作者:Priyesh Patel 譯者:Chor showImg(https://segmentfault.com/img...
閱讀 5029·2021-09-07 09:58
閱讀 780·2019-08-30 15:55
閱讀 2908·2019-08-30 15:55
閱讀 915·2019-08-30 15:53
閱讀 1549·2019-08-29 12:57
閱讀 1796·2019-08-26 13:46
閱讀 558·2019-08-26 11:00
閱讀 3657·2019-08-23 15:42