這 3個JS 性能基礎,讓 Bluebird 更快速
正如我們在近期的文章《 Promises made by a Reaktor devloper had an impact on the industry 》中許諾的那樣,以下是我們 Petka Antonov – 程序員和備受讚譽的 Bluebird promise 庫的創造者,分享的一些原創知識。
Bluebird 是一個被廣泛使用的 JS promise 庫,最初被注意到是在 2013 年,因其實施速度比當時類似功能的其它 promise 庫快了 100 倍。Bluebird 如此之快的原因在於,它對 JavaScript 優化的基礎原理的運用貫穿了整個庫。本文將詳細介紹三種用於優化 Bluebird 的最有價值的基礎知識。
1. 函數對象分配最小化
對象分配,尤其是函數對象分配,在實現時由於產生大量的內部數據,對性能造成沉重的負擔。JavaScript 的實際實現是一種垃圾回收機制,所以分配的對象並不是簡單地存儲在內存中,垃圾回收器在不斷地尋找未使用的對象,從而釋放它們。在 JavaScript 中使用的內存越多,垃圾回收器佔用的 CPU 資源也就越多,從而執行實際代碼的 CPU 也就越少。
在 JavaScript 中,函數是第一類對象。這意味著它們和任何其它對象一樣,具有相同的特徵和屬性。如果你有一個包含了另一個或多個函數的代碼聲明的函數,那麼對父函數的每一次調用都會創建新的、唯一的函數對象,儘管執行了相同的代碼。如下是一個基本的例子:
functiontrim(string){
functiontrimStart(string){
returnstring.replace(/^s+/g,"");
}
functiontrimEnd(string){
returnstring.replace(/s+$/g,"");
}
returntrimEnd(trimStart(string))
}
現在每次調用 trim 函數,都會創建兩個不必要的函數對象來表示 trimStrat 和 trimEnd。函數對象是不必要的,因為它們並不用於對象的唯一標識,例如屬性賦值或變數封裝。他們只用於代碼所包含的功能。
這個例子很容易優化,只需簡單地把這些函數移出 trim 。由於示例包含在模塊當中,並且模塊僅為程序載入一次,所以函數將只存在一種表現形式:
functiontrimStart(string){
returnstring.replace(/^s+/g,"");
}
functiontrimEnd(string){
returnstring.replace(/s+$/g,"");
}
functiontrim(string){
returntrimEnd(trimStart(string))
}
然而,更多常見的函數對象似乎是一個無法避免的弊病,沒辦法這麼輕易地優化。例如,任何時候你傳遞一個稍後會被調用的回調函數,幾乎總是需要一個特定的上下文來進行回調。通常情況下,這種上下文以簡單而直觀但低效的閉包方式來實現。一個簡單的例子就是使用默認的非同步回調介面在節點中讀取一個 JSON 文件:
varfs=require( fs );
functionreadFileAsJson(fileName,callback){
fs.readFile(fileName, utf8 ,function(error,result){
// This is a new function object created every time readFileAsJson is called
//每當 readFileAsJson 被調用,都會創建新的函數對象
// Since it s a closure, an internal Context object is also
// allocated for the closure state
//由於這是一個閉包,一個內部的上下文對象也會分配給這個閉包狀態
if(error){
returncallback(error);
}
// The try-catch block is needed to handle a possible syntax error from invalid JSON
//try-catch塊用於處理由於無效的JSON文件導致的可能出現的語法錯誤
try{
varjson=JSON.parse(result);
callback(null,json);
}catch(e){
callback(e);
}
})
}
在這個例子中,傳遞給 fs.readFile 的回調函數不能被移出 readFileAsJson,因為它通過唯一的變數回調創建了一個閉包。還應該注意的是,fs.readFile 無論是作為命名函數聲明還是內聯匿名函數來都沒什麼區別。
很大程度上 Bluebird 內部使用的優化是使用顯示的簡單對象來保存上下文數據。由一個通過多層的回調組成的操作,只需要一次這樣的對象分配。每次將回調傳遞到另一個層時,每個層將不會創建一個新的閉包,而是將顯式的簡單對象作為一個額外的參數進行傳遞。舉個例子,假設一個操作中有五個回調的步驟,使用閉包意味著分配五個函數對象和上下文對象,但是如果使用這個plain object方法來優化的話,總共只分配一個 plain object 就可以了。
我們可以修改一下 fs.readFile API,讓它接受一個上下文對象,在剛剛的例子中使用這種優化方法以後,代碼看起來就是這樣的:
varfs=require( fs-modified );
functioninternalReadFileCallback(error,result){
// The modified readFile calls the callback with the context object set to `this`,
// which is just the original client s callback function
//重新修改後的 readFile 調用了帶有設置為 「this」 的上下文對象的回調函數
//也就是原始的客戶端的回調函數
if(error){
returnthis(error);
}
// The try-catch block is needed to handle a possible syntax error from invalid JSON
//try-catch塊用於處理由於無效的JSON文件導致的可能出現的語法錯誤
try{
varjson=JSON.parse(result);
this(null,json);
}catch(e){
this(e);
}
}
functionreadFileAsJson(fileName,callback){
// The modified fs.readFile would take the context object as 4th argument.
//修改後的 fs.readFile 會將這個上下文對象作為第四個參數
// There is no need to create a separate plain object to contain `callback` so it
//這裡沒有必要創建一個獨立的 plain object 來包含 callback
// can just be made the context object directly.
//可以直接作為這個上下文對象
fs.readFile(fileName, utf8 ,internalReadFileCallback,callback);
}
2. 對象大小最小化
最小化那些經常並大量分配的對象(如 promises)的大小是至關重要的。在最常用的 JavaScript 實現中,對象分配的堆被分成不同的段和空間。較小的對象比較大的對象需要更多的時間來填滿這些空間和段,從而減少了垃圾回收器的工作量。在決定對象有效或失效時,較小的對象可以使垃圾回收器訪問更少的欄位。
布爾和/或受限制的整數欄位時,可以通過位運算符打包到一個更小的空間。JavaScript 位運算符可以操作 32 位的整數,因此你可以將 32 個布爾欄位或 8 個 4 位的整數或 16 個布爾值和 2 個 8 位的整數等合并到一個欄位中。為了保持代碼的可讀性,每一個邏輯欄位都應該有一對 getter 和 setter 函數來執行物理欄位的正確的位操作。將一個布爾欄位打包為一個整數(未來可以擴大到容納更多的邏輯欄位)的例子如下:
// Use 1
constREADONLY=1
classFile{
constructor(){
this._bitField=;
}
isReadOnly(){
// Parentheses are required.
//括弧是必須的
return(this._bitField&READONLY)!==;
}
setReadOnly(){
this._bitField=this._bitFieldREADONLY;
}
unsetReadOnly(){
this._bitField=this._bitField&(~READONLY);
}
}
這個存取方法非常短,因此它們在運行時很有可能被內聯,從而不涉及函數調用的開銷。
當使用一個布爾值來追蹤這個欄位保存的是哪種類型的值的時候,兩個或多個從來不會同時使用的欄位就可以壓縮為一個欄位。然而,像之前描述的辦法一樣,將這個布爾欄位作為一個打包好的整數欄位來執行僅僅是節約了一些空間而已。
在 Bulebird 中,這個技巧被用來儲存 promise 執行的返回值或拒絕原因。這裡並沒有明顯的分界:如果 promise 執行成功,那麼這個返回值可能會被存儲在失敗的回調函數的區域;請求失敗時,拒絕請求的原因也可能存儲在成功的回調函數區域。再重複一次,所有的訪問都應該通過存取函數來隱藏這些難看的優化細節。
如果一個對象請求了一系列值,你可以通過將這些值直接儲存在對象的索引中,從而避免一個單獨的數組分配。
所以,不要這樣:
classEventEmitter{
constructor(){
this.listeners=[];
}
addListener(fn){
}
}
你可以避免使用數組:
classEventEmitter{
constructor(){
this.length=;
}
addListener(fn){
varindex=this.length;
this.length++;
this[index]=fn;
}
}
如果 .length 可以被限制到一個很小的整數(比如 10 位,這將會把事件發射器限制為最多 1024 個監聽器),那麼它就可以和其他的受限制的整數或布爾值一起壓縮成為一個打包好的欄位。
3. 使用空函數並簡單地覆蓋它們來實現代價昂貴的可選功能
比起不管監控功能是否啟用都調用鉤子函數,在調用前先檢查一下監控功能是否啟用要好得多。然而,由於內置緩存和函數內聯,對於不啟用該功能的用戶,實際上可以完全消除成本。這可以通過將原始的 hook 方法設置為空操作函數來實現:
classPromise{
// ...
constructor(executor){
// ...
this._promiseCreatedHook();
}
// Just an empty no-op method.
_promiseCreatedHook(){}
}
現在,如果用戶沒有啟用監控功能,優化器就會發現這個函數調用不執行任何操作,並將其消除。所以對 constructor 中 hook 方法的有效調用並不存在。
為了使這個功能真正地執行,啟用該功能時必須用真正的實現覆蓋所有相關的空操作函數:
functionenableMonitoringFeature(){
Promise.prototype._promiseCreatedHook=function(){
// Actual implementation here
};
// ...
}
像這樣的覆蓋方法將會使所有 Promise 類的對象建立的內置緩存失效,因此只應該在啟動應用程序時,在所有 promise 對象創建以前執行。如果在任一內存使用之前發生了覆蓋,那麼在功能被啟用後的操作創建的內置緩存也不會識別到這些空操作函數的存在。
覺得本文對你有幫助?請分享給更多人
關注「前端大全」,提升前端技能
TAG:前端大全 |
※BeatifulSoup,Xpath,CSS 選擇器的性能比較
※Galaxy S9樹立Android性能新標杆 但iPhone X仍然比它快
※Galaxy Note9 VS iPhone X,誰性能更勝一籌?
※Oculus Go性能比三星Galaxy S7「明顯更好」?
※Salesforce 5大性能問題
※三星Galaxy Note 9發布了,但性能拼不過iPhone X
※MySQL使用JPA+Hibernate的9個高性能技巧
※iPhone 7 Plus和iPhone X,性能區別?
※在Salesforce Lightning Experience提高性能和速度
※iPhone7Plus成性價比最高的大屏iPhone!價格便宜,性能好!
※iPhoneX和iPhone8P性能大比拼,再也不用選擇困難了!
※性能怪獸!全新配色 Air Jordan 18 「Yellow Suede」 實物亮相
※iPhone X Plus基準測試 性能大幅超越Android產品
※微服務網關哪家強?一文看懂Zuul, Nginx, Spring Cloud, Linkerd性能差異
※oppoFindX的性能和vivoNEX的性能哪個好
※Linux Kernel 更新,網路性能大幅提高
※要性能還是要儲存:iPhone8 plus對比iPhone7 plus你會怎麼選
※2018年iPhone新款只有iPhoneX plus,性能很強悍,價格更強悍!
※Volvo S60 T8 Polestar Engineeered,北極星性能轎跑!
※Galaxy S9 Plus與iPhone X性能速度實測,S9表現相當亮眼!