你不懂js系列學習筆記-異步與性能- 05

第五章: 程序性能

原文:You-Dont-Know-JSjavascript

這本書至此一直是關於如何更有效地利用異步模式。可是咱們尚未直接解釋爲何異步對於 JS 如此重要。最明顯明確的理由就是 性能html

舉個例子,若是你要發起兩個 Ajax 請求,並且他們是相互獨立的,但你在進行下一個任務以前須要等到他們所有完成,你就有兩種選擇來對這種互動創建模型:順序和併發。html5

你能夠發起第一個請求並等到它完成再發起第二個請求。或者,就像咱們在 promise 和 generator 中看到的那樣,你能夠「並列地」發起兩個請求,並在繼續下一步以前讓一個「門」等待它們所有完成。java

顯然,後者要比前者性能更好。而更好的性能通常都會帶來更好的用戶體驗。node

異步(併發穿插)甚至可能僅僅加強高性能的印象,即使整個程序依然要用相同的時間才成完成。用戶對性能的印象意味着一切——若是不能再多的話!——和實際可測量的性能同樣重要。git

如今,咱們想超越局部的異步模式,轉而在程序級別的水平上討論一些宏觀的性能細節。github

注意: 你可能會想知道關於微性能問題,好比a++++a哪一個更快。咱們會在下一章「基準分析與調優」中討論這類性能細節。web

1. Web Workers

若是你有一些處理密集型的任務,但你不想讓它們在主線程上運行(那樣會使瀏覽器/UI 變慢),你可能會但願 JavaScript 能夠以多線程的方式操做。算法

在第一章中,咱們詳細地談到了關於 JavaScript 如何是單線程的。那仍然是成立的。可是單線程不是組織你程序運行的惟一方法。編程

想象將你的程序分割成兩塊兒,在 UI 主線程上運行其中的一起,而在一個徹底分離的線程上運行另外一塊兒。

這樣的結構會引起什麼咱們須要關心的問題?

其一,你會想知道運行在一個分離的線程上是否意味着它在並行運行(在多 CPU/內核的系統上),如此在第二個線程上長時間運行的處理將 不會 阻塞主程序線程。不然,「虛擬線程」所帶來的好處,不會比咱們已經在異步併發的 JS 中獲得的更多。

並且你會想知道這兩塊兒程序是否訪問共享的做用域/資源。若是是,那麼你就要對付多線程語言(Java,C++等等)的全部問題,好比協做式或搶佔式鎖定(互斥,等)。這是不少額外的工做,並且不該當輕易着手。

換一個角度,若是這兩塊兒程序不能共享做用域/資源,你會想知道它們將如何「通訊」。

全部這些咱們須要考慮的問題,指引咱們探索一個在近 HTML5 時代被加入 web 平臺的特性,稱爲「Web Worker」。這是一個瀏覽器(也就是宿主環境)特性,並且幾乎和 JS 語言自己沒有任何關係。也就是說,JavaScript 當前 並無任何特性能夠支持多線程運行。

可是一個像你的瀏覽器那樣的環境能夠很容易地提供多個 JavaScript 引擎實例,每一個都在本身的線程上,並容許你在每一個線程上運行不一樣的程序。你的程序中分離的線程塊兒中的每個都稱爲一個「(Web)Worker」。這種並行機制叫作「任務並行機制」,它強調將你的程序分割成塊兒來並行運行。

在你的主 JS 程序(或另外一個 Worker)中,你能夠這樣初始化一個 Worker:

var w1 = new Worker("http://some.url.1/mycoolworker.js");
複製代碼

這個 URL 應當指向 JS 文件的位置(不是一個 HTML 網頁!),它將會被加載到一個 Worker。而後瀏覽器會啓動一個分離的線程,讓這個文件在這個線程上做爲獨立的程序運行。

注意: 這種用這樣的 URL 建立的 Worker 稱爲「專用(Dedicated)Wroker」。但與提供一個外部文件的 URL 不一樣的是,你也能夠經過提供一個 Blob URL(另外一個 HTML5 特性)來建立一個「內聯(Inline)Worker」;它實質上是一個存儲在單一(二進制)值中的內聯文件。可是,Blob 超出了咱們要在這裏討論的範圍。

Worker 不會相互,或者與主程序共享任何做用域或資源——那會將全部的多線程編程的噩夢帶到咱們面前——取而代之的是一種鏈接它們的基本事件消息機制。

w1Worker 對象是一個事件監聽器和觸發器,它容許你監聽 Worker 發出的事件也容許你向 Worker 發送事件。

這是如何監聽事件(實際上,是固定的"message"事件):

w1.addEventListener("message", function(evt) {
  // evt.data
});
複製代碼

並且你能夠發送"message"事件給 Worker:

w1.postMessage("something cool to say");
複製代碼

在 Worker 內部,消息是徹底對稱的:

// "mycoolworker.js"

addEventListener("message", function(evt) {
  // evt.data
});

postMessage("a really cool reply");
複製代碼

要注意的是,一個專用 Worker 與它建立的程序是一對一的關係。也就是,"message"事件不須要消除任何歧義,由於咱們能夠肯定它只可能來自於這種一對一關係——不是從 Wroker 來的,就是從主頁面來的。

一般主頁面的程序會建立 Worker,可是一個 Worker 能夠根據須要初始化它本身的子 Worker——稱爲 subworker。有時將這樣的細節委託給一個「主」Worker 十分有用,它能夠生成其餘 Worker 來處理任務的一部分。不幸的是,在本書寫做的時候,Chrome 尚未支持 subworker,然而 Firefox 支持。

要從建立一個 Worker 的程序中當即殺死它,能夠在 Worker 對象(就像前一個代碼段中的w1)上調用terminate()。忽然終結一個 Worker 線程不會給它任何機會結束它的工做,或清理任何資源。這和你關閉瀏覽器的標籤頁來殺死一個頁面類似。

若是你在瀏覽器中有兩個或多個頁面(或者打開同一個頁面的多個標籤頁!),試着從同一個文件 URL 中建立 Worker,實際上最終結果是徹底分離的 Worker。待一下子咱們就會討論「共享」Worker 的方法。

注意: 看起來一個惡意的或者是呆頭呆腦的 JS 程序能夠很容易地經過在系統上生成數百個 Worker 來發起拒絕服務攻擊(Dos 攻擊),看起來每一個 Worker 都在本身的線程上。雖然一個 Worker 將會在存在於一個分離的線程上是有某種保證的,但這種保證不是沒有限制的。系統能夠自由決定有多少實際的線程/CPU/內核要去建立。沒有辦法預測或保證你能訪問多少,雖然不少人假定它至少和可用的 CPU/內核數同樣多。我認爲最安全的臆測是,除了主 UI 線程外至少有一個線程,僅此而已。

Worker 環境

在 Worker 內部,你不能訪問主程序的任何資源。這意味着你不能訪問它的任何全局變量,你也不能訪問頁面的 DOM 或其餘資源。記住:它是一個徹底分離的線程。

然而,你能夠實施網絡操做(Ajax,WebSocket)和設置定時器。另外,Worker 能夠訪問它本身的幾個重要全局變量/特性的拷貝,包括navigatorlocationJSON,和applicationCache

你還可使用importScripts(..)加載額外的 JS 腳本到你的 Worker 中:

// 在Worker內部
importScripts("foo.js", "bar.js");
複製代碼

這些腳本會被同步地加載,這意味着在文件完成加載和運行以前,importScripts(..)調用會阻塞 Worker 的執行。

注意: 還有一些關於暴露<canvas>API 給 Worker 的討論,其中包括使 canvas 成爲 Transferable 的(見「數據傳送」一節),這將容許 Worker 來實施一些精細的脫線程圖形處理,在高性能的遊戲(WebGL)和其餘相似應用中可能頗有用。雖然這在任何瀏覽器中都還不存在,可是頗有可能在近將來發生。

Web Worker 的常見用途是什麼?

  • 處理密集型的數學計算
  • 大數據集合的排序
  • 數據操做(壓縮,音頻分析,圖像像素操做等等)
  • 高流量網絡通訊

數據傳送

你可能注意到了這些用途中的大多數的一個共同性質,就是它們要求使用事件機制穿越線程間的壁壘來傳遞大量的信息,也許是雙向的。

在 Worker 的早期,將全部數據序列化爲字符串是惟一的選擇。除了在兩個方向上進行序列化時速度上變慢了,另一個主要缺點是,數據是被拷貝的,這意味着內存用量翻了一倍(以及在後續垃圾回收上的流失)。

謝天謝地,如今咱們有了幾個更好的選擇。

若是你傳遞一個對象,在另外一端一個所謂的「結構化克隆算法(Structured Cloning Algorithm)」(developer.mozilla.org/en-US/docs/… )會用於拷貝/複製這個對象。這個算法至關精巧,甚至能夠處理帶有循環引用的對象複製。to-string/from-string 的性能劣化沒有了,但用這種方式咱們依然面對着內存用量的翻倍。IE10 以上版本,和其餘主流瀏覽器都對此有支持。

一個更好的選擇,特別是對大的數據集合而言,是「Transferable 對象」(updates.html5rocks.com/2011/12/Tra… )。它使對象的「全部權」被傳送,而對象自己沒動。一旦你傳送一個對象給 Worker,它在原來的位置就空了出來或者不可訪問——這消除了共享做用域的多線程編程中的災難。固然,全部權的傳送能夠雙向進行。

選擇使用 Transferable 對象不須要你作太多;任何實現了 Transferable 接口(developer.mozilla.org/en-US/docs/… )的數據結構都將自動地以這種方式傳遞(Firefox 和 Chrome 支持此特性)。

舉個例子,有類型的數組如Uint8Array(見本系列的 ES6 與將來)是一個「Transferables」。這是你如何用postMessage(..)來傳送一個 Transferable 對象:

// `foo` 是一個 `Uint8Array`

postMessage(foo.buffer, [foo.buffer]);
複製代碼

第一個參數是未經加工的緩衝,而第二個參數是要傳送的內容的列表。

不支持 Transferable 對象的瀏覽器簡單地降級到結構化克隆,這意味着性能上的下降,而不是完全的特性失靈。

2. SIMD

一個指令,多個數據(SIMD)是一種「數據並行機制」形式,與 Web Worker 的「任務並行機制」相對應,由於他強調的不是程序邏輯的塊兒被並行化,而是多個字節的數據被並行地處理。

使用 SIMD,線程不提供並行機制。相反,現代 CPU 用數字的「向量」提供 SIMD 能力——想一想:指定類型的數組——還有能夠在全部這些數字上並行操做的指令;這些是利用底層操做的指令級別的並行機制。

使 SIMD 能力包含在 JavaScript 中的努力主要是由 Intel 帶頭的(01.org/node/1495 ),名義上是 Mohammad Haghighat(在本書寫做的時候),與 Firefox 和 Chrome 團隊合做。SIMD 處於早期標準化階段,並且頗有可能被加入將來版本的 JavaScript 中,極可能在 ES7 的時間框架內。

SIMD JavaScript 提議向 JS 代碼暴露短向量類型與 API,它們在 SIMD 可用的系統中將操做直接映射爲 CPU 指令的等價物,同時在非 SIMD 系統中退回到非並行化操做的「shim」。

對於數據密集型的應用程序(信號分析,對圖形的矩陣操做等等)來講,這種並行數學處理在性能上的優點是十分明顯的!

在本書寫做時,SIMD API 的早期提案形式看起來像這樣:

var v1 = SIMD.float32x4(3.14159, 21.0, 32.3, 55.55);
var v2 = SIMD.float32x4(2.1, 3.2, 4.3, 5.4);

var v3 = SIMD.int32x4(10, 101, 1001, 10001);
var v4 = SIMD.int32x4(10, 20, 30, 40);

SIMD.float32x4.mul(v1, v2); // [ 6.597339, 67.2, 138.89, 299.97 ]
SIMD.int32x4.add(v3, v4); // [ 20, 121, 1031, 10041 ]
複製代碼

這裏展現了兩種不一樣的向量數據類型,32 位浮點數和 32 位整數。你能夠看到這些向量正好被設置爲 4 個 32 位元素,這與大多數 CPU 中可用的 SIMD 向量的大小(128 位)相匹配。在將來咱們看到一個x8(或更大!)版本的這些 API 也是可能的。

除了mul()add(),許多其餘操做也極可能被加入,好比sub()div()abs()neg()sqrt()reciprocal()reciprocalSqrt() (算數運算),shuffle()(重拍向量元素),and()or()xor()not()(邏輯運算),equal()greaterThan()lessThan() (比較運算),shiftLeft()shiftRightLogical()shiftRightArithmetic()(輪換),fromFloat32x4(),和fromInt32x4()(變換)。

注意: 這裏有一個 SIMD 功能的官方「填補」(頗有但願,預期的,着眼將來的填補)(github.com/johnmccutch… ),它描述了許多比咱們在這一節中沒有講到的許多計劃中的 SIMD 功能。

3. asm.js

「asm.js」(asmjs.org/ )是能夠被高度優化的 JavaScript 語言子集的標誌。經過當心地迴避那些特定的很難優化的(垃圾回收,強制轉換,等等)機制和模式,asm.js 風格的代碼能夠被 JS 引擎識別,並且用主動地底層優化進行特殊的處理。

與本章中討論的其餘性能優化機制不一樣的是,asm.js 沒必需要是必須被 JS 語言規範所採納的東西。確實有一個 asm.js 規範(asmjs.org/spec/latest… ),但它主要是追蹤一組關於優化的候選對象的推論,而不是 JS 引擎的需求。

目前尚未新的語法被提案。取而代之的是,ams.js 建議了一些方法,用來識別那些符合 ams.js 規則的既存標準 JS 語法,而且讓引擎相應地實現它們本身的優化功能。

關於 ams.js 應當如何在程序中活動的問題,在瀏覽器生產商之間存在一些爭議。早期版本的 asm.js 實驗中,要求一個"use asm";編譯附註(與 strict 模式的"use strict";相似)來幫助 JS 引擎來尋找 asm.js 優化的機會和提示。另外一些人則斷言 asm.js 應當只是一組啓發式算法,讓引擎自動地識別而不用做者作任何額外的事情,這意味着理論上既存的程序能夠在不用作任何特殊的事情的狀況下從 asm.js 優化中獲益。

如何使用 asm.js 進行優化

關於 asm.js 須要理解的第一件事情是類型和強制轉換。若是 JS 引擎不得不在變量的操做期間一直追蹤一個變量內的值的類型,以便於在必要時它能夠處理強制轉換,那麼就會有許多額外的工做使程序處於次優化狀態。

注意: 爲了說明的目的,咱們將在這裏使用 ams.js 風格的代碼,但要意識到的是你手寫這些代碼的狀況不是很常見。asm.js 的本意更多的是做爲其餘工具的編譯目標,好比 Emscripten(github.com/kripken/ems… )。固然你寫本身的 asm.js 代碼也是可能的,可是這一般不是一個好主意,由於那樣的代碼很是底層,而這意味着它會很是耗時並且易錯。儘管如此,也會有狀況使你想要爲了 ams.js 優化的目的手動調整代碼。

這裏有一些「技巧」,你可使用它們來提示支持 asm.js 的 JS 引擎變量/操做預期的類型是什麼,以便於它能夠跳過那些強制轉換追蹤的步驟。

舉個例子:

var a = 42;

// ..

var b = a;
複製代碼

在這個程序中,賦值b = a在變量中留下了類型分歧的問題。然而,它能夠寫成這樣:

var a = 42;

// ..

var b = a | 0;
複製代碼

這裏,咱們與值0一塊兒使用了|(「二進制或」),雖然它對值沒有任何影響,但它確保這個值是一個 32 位整數。這段代碼在普通的 JS 引擎中能夠工做,可是當它運行在支持 asm.js 的 JS 引擎上時,它 能夠 表示b應當老是被做爲 32 位整數來對待,因此強制轉換追蹤能夠被跳過。

相似地,兩個變量之間的加法操做能夠被限定爲性能更好的整數加法(而不是浮點數):

(a + b) | 0;
複製代碼

再一次,支持 asm.js 的 JS 引擎能夠看到這個提示,並推斷+操做應當是一個 32 位整數加法,由於不論怎樣整個表達式的最終結果都將自動是 32 位整數。

複習

本書的前四章基於這樣的前提:異步編碼模式給了你編寫更高效代碼的能力,這一般是一個很是重要的改進。可是異步行爲也就能幫你這麼多,由於它在基礎上仍然使用一個單獨的事件輪詢線程。

因此在這一章咱們涵蓋了幾種程序級別的機制來進一步提高性能。

Web Worker 讓你在一個分離的線程上運行一個 JS 文件(也就是程序),使用異步事件在線程之間傳遞消息。對於將長時間運行或資源密集型任務掛載到一個不一樣線程,從而讓主 UI 線程保持相應來講,它們很是棒。

SIMD 提議將 CPU 級別的並行數學操做映射到 JavaScript API 上來提供高性能數據並行操做,好比在大數據集合上進行數字處理。

最後,asm.js 描述了一個 JavaScript 的小的子集,它迴避了 JS 中不易優化的部分(好比垃圾回收與強制轉換)並讓 JS 引擎經過主動優化識別並運行這樣的代碼。asm.js 能夠手動編寫,可是極其麻煩且易錯,就像手動編寫彙編語言。相反,asm.js 的主要意圖是做爲一個從其餘高度優化的程序語言交叉編譯來的目標——例如,Emscripten(github.com/kripken/ems… )能夠將 C/C++轉譯爲 JavaScript。

雖然在本章沒有明確地說起,在很早之前的有關 JavaScript 的討論中存在着更激進的想法,包括近似地直接多線程功能(不只僅是隱藏在數據結構 API 後面)。不管這是否會明確地發生,仍是咱們將看到更多並行機制偷偷潛入 JS,可是在 JS 中發生更多程序級別優化的將來是能夠肯定的。

相關文章
相關標籤/搜索