# 第五章: 程序性能
這本書至此一直是關于如何更有效地利用異步模式。但是我們還沒有直接解釋為什么異步對于JS如此重要。最明顯明確的理由就是?性能。
舉個例子,如果你要發起兩個Ajax請求,而且他們是相互獨立的,但你在進行下一個任務之前需要等到他們全部完成,你就有兩種選擇來對這種互動建立模型:順序和并發。
你可以發起第一個請求并等到它完成再發起第二個請求。或者,就像我們在promise和generator中看到的那樣,你可以“并列地”發起兩個請求,并在繼續下一步之前讓一個“門”等待它們全部完成。
顯然,后者要比前者性能更好。而更好的性能一般都會帶來更好的用戶體驗。
異步(并發穿插)甚至可能僅僅增強高性能的印象,即便整個程序依然要用相同的時間才成完成。用戶對性能的印象意味著一切——如果不能再多的話!——和實際可測量的性能一樣重要。
現在,我們想超越局部的異步模式,轉而在程序級別的水平上討論一些宏觀的性能細節。
注意:?你可能會想知道關于微性能問題,比如`a++`與`++a`哪個更快。我們會在下一章“基準分析與調優”中討論這類性能細節。
## Web Workers
如果你有一些處理密集型的任務,但你不想讓它們在主線程上運行(那樣會使瀏覽器/UI變慢),你可能會希望JavaScript可以以多線程的方式操作。
在第一章中,我們詳細地談到了關于JavaScript如何是單線程的。那仍然是成立的。但是單線程不是組織你程序運行的唯一方法。
想象將你的程序分割成兩塊兒,在UI主線程上運行其中的一塊兒,而在一個完全分離的線程上運行另一塊兒。
這樣的結構會引發什么我們需要關心的問題?
其一,你會想知道運行在一個分離的線程上是否意味著它在并行運行(在多CPU/內核的系統上),如此在第二個線程上長時間運行的處理將?不會?阻塞主程序線程。否則,“虛擬線程”所帶來的好處,不會比我們已經在異步并發的JS中得到的更多。
而且你會想知道這兩塊兒程序是否訪問共享的作用域/資源。如果是,那么你就要對付多線程語言(Java,C++等等)的所有問題,比如協作式或搶占式鎖定(互斥,等)。這是很多額外的工作,而且不應當輕易著手。
換一個角度,如果這兩塊兒程序不能共享作用域/資源,你會想知道它們將如何“通信”。
所有這些我們需要考慮的問題,指引我們探索一個在近HTML5時代被加入web平臺的特性,稱為“Web Worker”。這是一個瀏覽器(也就是宿主環境)特性,而且幾乎和JS語言本身沒有任何關系。也就是說,JavaScript?*當前*?并沒有任何特性可以支持多線程運行。
但是一個像你的瀏覽器那樣的環境可以很容易地提供多個JavaScript引擎實例,每個都在自己的線程上,并允許你在每個線程上運行不同的程序。你的程序中分離的線程塊兒中的每一個都稱為一個“(Web)Worker”。這種并行機制叫做“任務并行機制”,它強調將你的程序分割成塊兒來并行運行。
在你的主JS程序(或另一個Worker)中,你可以這樣初始化一個Worker:
```source-js
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不會相互,或者與主程序共享任何作用域或資源——那會將所有的多線程編程的噩夢帶到我們面前——取而代之的是一種連接它們的基本事件消息機制。
`w1`Worker對象是一個事件監聽器和觸發器,它允許你監聽Worker發出的事件也允許你向Worker發送事件。
這是如何監聽事件(實際上,是固定的`"message"`事件):
```source-js
w1.addEventListener( "message", function(evt){
// evt.data
} );
```
而且你可以發送`"message"`事件給Worker:
```source-js
w1.postMessage( "something cool to say" );
```
在Worker內部,消息是完全對稱的:
```source-js
// "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可以訪問它自己的幾個重要全局變量/特性的拷貝,包括`navigator`,`location`,`JSON`,和`applicationCache`。
你還可以使用`importScripts(..)`加載額外的JS腳本到你的Worker中:
```source-js
// 在Worker內部
importScripts( "foo.js", "bar.js" );
```
這些腳本會被同步地加載,這意味著在文件完成加載和運行之前,`importScripts(..)`調用會阻塞Worker的執行。
注意:?還有一些關于暴露`<canvas>`API給Worker的討論,其中包括使canvas成為Transferable的(見“數據傳送”一節),這將允許Worker來實施一些精細的脫線程圖形處理,在高性能的游戲(WebGL)和其他類似應用中可能很有用。雖然這在任何瀏覽器中都還不存在,但是很有可能在近未來發生。
Web Worker的常見用途是什么?
* 處理密集型的數學計算
* 大數據集合的排序
* 數據操作(壓縮,音頻分析,圖像像素操作等等)
* 高流量網絡通信
### 數據傳送
你可能注意到了這些用途中的大多數的一個共同性質,就是它們要求使用事件機制穿越線程間的壁壘來傳遞大量的信息,也許是雙向的。
在Worker的早期,將所有數據序列化為字符串是唯一的選擇。除了在兩個方向上進行序列化時速度上變慢了,另外一個主要缺點是,數據是被拷貝的,這意味著內存用量翻了一倍(以及在后續垃圾回收上的流失)。
謝天謝地,現在我們有了幾個更好的選擇。
如果你傳遞一個對象,在另一端一個所謂的“結構化克隆算法(Structured Cloning Algorithm)”(https://developer.mozilla.org/en-US/docs/Web/Guide/API/DOM/The_structured_clone_algorithm )會用于拷貝/復制這個對象。這個算法相當精巧,甚至可以處理帶有循環引用的對象復制。to-string/from-string的性能劣化沒有了,但用這種方式我們依然面對著內存用量的翻倍。IE10以上版本,和其他主流瀏覽器都對此有支持。
一個更好的選擇,特別是對大的數據集合而言,是“Transferable對象”(http://updates.html5rocks.com/2011/12/Transferable-Objects-Lightning-Fast )。它使對象的“所有權”被傳送,而對象本身沒動。一旦你傳送一個對象給Worker,它在原來的位置就空了出來或者不可訪問——這消除了共享作用域的多線程編程中的災難。當然,所有權的傳送可以雙向進行。
選擇使用Transferable對象不需要你做太多;任何實現了Transferable接口(https://developer.mozilla.org/en-US/docs/Web/API/Transferable )的數據結構都將自動地以這種方式傳遞(Firefox和Chrome支持此特性)。
舉個例子,有類型的數組如`Uint8Array`(見本系列的?*ES6與未來*)是一個“Transferables”。這是你如何用`postMessage(..)`來傳送一個Transferable對象:
```source-js
// `foo` 是一個 `Uint8Array`
postMessage( foo.buffer, [ foo.buffer ] );
```
第一個參數是未經加工的緩沖,而第二個參數是要傳送的內容的列表。
不支持Transferable對象的瀏覽器簡單地降級到結構化克隆,這意味著性能上的降低,而不是徹底的特性失靈。
### 共享的Workers
如果你的網站或應用允許多個標簽頁加載同一個網頁(一個常見的特性),你也許非常想通過防止復制專用Worker來降低系統資源的使用量;這方面最常見的資源限制是網絡套接字鏈接,因為瀏覽器限制同時連接到一個服務器的連接數量。當然,限制從客戶端來的鏈接數也緩和了你的服務器資源需求。
在這種情況下,創建一個單獨的中心化Worker,讓你的網站或應用的所有網頁實例可以?*共享*?它是十分有用的。
這稱為`SharedWorker`,你會這樣創建它(僅有Firefox與Chrome支持此特性):
```source-js
var w1 = new SharedWorker( "http://some.url.1/mycoolworker.js" );
```
因為一個共享Worker可以連接或被連接到你的網站上的多個程序實例或網頁,Worker需要一個方法來知道消息來自哪個程序。這種唯一的標識稱為“端口(port)”——聯想網絡套接字端口。所以調用端程序必須使用Worker的`port`對象來通信:
```source-js
w1.port.addEventListener( "message", handleMessages );
// ..
w1.port.postMessage( "something cool" );
```
另外,端口連接必須被初始化,就像這樣:
```source-js
w1.port.start();
```
在共享Worker內部,一個額外的事件必須被處理:`"connect"`。這個事件為這個特定的連接提供端口`object`。保持多個分離的連接最簡單的方法是在`port`上使用閉包,就像下面展示的那樣,同時在`"connect"`事件的處理器內部定義這個連接的事件監聽與傳送:
```source-js
// 在共享Worker的內部
addEventListener( "connect", function(evt){
// 為這個連接分配的端口
var port = evt.ports[0];
port.addEventListener( "message", function(evt){
// ..
port.postMessage( .. );
// ..
} );
// 初始化端口連接
port.start();
} );
```
除了這點不同,共享與專用Worker的功能和語義是一樣的。
注意:?如果在一個端口的連接終結時還有其他端口的連接存活著的話,共享Worker也會存活下來,而專用Worker會在與初始化它的程序間接終結時終結。
### 填補 Web Workers
對于并行運行的JS程序在性能考量上,Web Worker十分吸引人。然而,你的代碼可能運行在對此缺乏支持的老版本瀏覽器上。因為Worker是一個API而不是語法,所以在某種程度上它們可以被填補。
如果瀏覽器不支持Worker,那就根本沒有辦法從性能的角度來模擬多線程。Iframe通常被認為可以提供并行環境,但在所有的現代瀏覽器中它們實際上和主頁運行在同一個線程上,所以用它們來模擬并行機制是不夠的。
正如我們在第一章中詳細討論的,JS的異步能力(不是并行機制)來自于事件輪詢隊列,所以你可以用計時器(`setTimeout(..)`等等)來強制模擬的Worker是異步的。然后你只需要提供Worker API的填補就行了。這里有一份列表(https://github.com/Modernizr/Modernizr/wiki/HTML5-Cross-Browser-Polyfills#web-workers ),但坦白地說它們看起來都不怎么樣。
我在這里(https://gist.github.com/getify/1b26accb1a09aa53ad25 )寫了一個填補`Worker`的輪廓。它很基礎,但應該滿足了簡單的`Worker`支持,它的雙向信息傳遞可以正確工作,還有`"onerror"`處理。你可能會擴展它來支持更多特性,比如`terminate()`或模擬共享Worker,只要你覺得合適。
注意:?你不能模擬同步阻塞,所以這個填補不允許使用`importScripts(..)`。另一個選擇可能是轉換并傳遞Worker的代碼(一旦Ajax加載后),來重寫一個`importScripts(..)`填補的一些異步形式,也許使用一個promise相關的接口。
## SIMD
一個指令,多個數據(SIMD)是一種“數據并行機制”形式,與Web Worker的“任務并行機制”相對應,因為他強調的不是程序邏輯的塊兒被并行化,而是多個字節的數據被并行地處理。
使用SIMD,線程不提供并行機制。相反,現代CPU用數字的“向量”提供SIMD能力——想想:指定類型的數組——還有可以在所有這些數字上并行操作的指令;這些是利用底層操作的指令級別的并行機制。
使SIMD能力包含在JavaScript中的努力主要是由Intel帶頭的(https://01.org/node/1495 ),名義上是Mohammad Haghighat(在本書寫作的時候),與Firefox和Chrome團隊合作。SIMD處于早期標準化階段,而且很有可能被加入未來版本的JavaScript中,很可能在ES7的時間框架內。
SIMD JavaScript提議向JS代碼暴露短向量類型與API,它們在SIMD可用的系統中將操作直接映射為CPU指令的等價物,同時在非SIMD系統中退回到非并行化操作的“shim”。
對于數據密集型的應用程序(信號分析,對圖形的矩陣操作等等)來說,這種并行數學處理在性能上的優勢是十分明顯的!
在本書寫作時,SIMD API的早期提案形式看起來像這樣:
```source-js
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功能的官方“填補”(很有希望,預期的,著眼未來的填補)(https://github.com/johnmccutchan/ecmascript_simd ),它描述了許多比我們在這一節中沒有講到的許多計劃中的SIMD功能。
## asm.js
“asm.js”(http://asmjs.org/ )是可以被高度優化的JavaScript語言子集的標志。通過小心地回避那些特定的很難優化的(垃圾回收,強制轉換,等等)機制和模式,asm.js風格的代碼可以被JS引擎識別,而且用主動地底層優化進行特殊的處理。
與本章中討論的其他性能優化機制不同的是,asm.js沒必須要是必須被JS語言規范所采納的東西。確實有一個asm.js規范(http://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(https://github.com/kripken/emscripten/wiki )。當然你寫自己的asm.js代碼也是可能的,但是這通常不是一個好主意,因為那樣的代碼非常底層,而這意味著它會非常耗時而且易錯。盡管如此,也會有情況使你想要為了ams.js優化的目的手動調整代碼。
這里有一些“技巧”,你可以使用它們來提示支持asm.js的JS引擎變量/操作預期的類型是什么,以便于它可以跳過那些強制轉換追蹤的步驟。
舉個例子:
```source-js
var a = 42;
// ..
var b = a;
```
在這個程序中,賦值`b = a`在變量中留下了類型分歧的問題。然而,它可以寫成這樣:
```source-js
var a = 42;
// ..
var b = a | 0;
```
這里,我們與值`0`一起使用了`|`(“二進制或”),雖然它對值沒有任何影響,但它確保這個值是一個32位整數。這段代碼在普通的JS引擎中可以工作,但是當它運行在支持asm.js的JS引擎上時,它?*可以*?表示`b`應當總是被作為32位整數來對待,所以強制轉換追蹤可以被跳過。
類似地,兩個變量之間的加法操作可以被限定為性能更好的整數加法(而不是浮點數):
```source-js
(a + b) | 0
```
再一次,支持asm.js的JS引擎可以看到這個提示,并推斷`+`操作應當是一個32位整數加法,因為不論怎樣整個表達式的最終結果都將自動是32位整數。
### asm.js 模塊
在JS中最托性能后腿的東西之一是關于內存分配,垃圾回收,與作用域訪問。asm.js對于這些問題建一個的一個方法是,聲明一個更加正式的asm.js“模塊”——不要和ES6模塊搞混;參見本系列的?*ES6與未來*。
對于一個asm.js模塊,你需要明確傳入一個被嚴格遵循的名稱空間——在規范中以`stdlib`引用,因為它應當代表需要的標準庫——來引入需要的符號,而不是通過詞法作用域來使用全局對象。在最基本的情況下,`window`對象就是一個可接受的用于asm.js模塊的`stdlib`對象,但是你可能應該構建一個更加被嚴格限制的對象。
你還必須定義一個“堆(heap)”——這只是一個別致的詞匯,它表示在內存中被保留的位置,變量不必要求內存分配或釋放已使用內存就可以使用——并將它傳入,這樣asm.js模塊就不必做任何導致內存流失的的事情;它可以使用提前保留的空間。
一個“堆”就像一個有類型的`ArrayBuffer`,比如:
```source-js
var heap = new ArrayBuffer( 0x10000 ); // 64k 的堆
```
使用這個提前保留的64k的二進制空間,一個asm.js模塊可以在這個緩沖區中存儲或讀取值,而不受任何內存分配與垃圾回收的性能損耗。比如,`heap`緩沖區可以在模塊內部用于備份一個64位浮點數值的數組,像這樣:
```source-js
var arr = new Float64Array( heap );
```
好了,讓我制作一個asm.js風格模塊的快速,愚蠢的例子來描述這些東西是如何聯系在一起的。我們將定義一個`foo(..)`,它為一個范圍接收一個開始位置(`x`)和一個終止位置(`y`),并且計算這個范圍內所有相鄰的數字的積,然后最終計算這些值的平均值:
```source-js
function fooASM(stdlib,foreign,heap) {
"use asm";
var arr = new stdlib.Int32Array( heap );
function foo(x,y) {
x = x | 0;
y = y | 0;
var i = 0;
var p = 0;
var sum = 0;
var count = ((y|0) - (x|0)) | 0;
// 計算范圍內所有相鄰的數字的積
for (i = x | 0;
(i | 0) < (y | 0);
p = (p + 8) | 0, i = (i + 1) | 0
) {
// 存儲結果
arr[ p >> 3 ] = (i * (i + 1)) | 0;
}
// 計算所有中間值的平均值
for (i = 0, p = 0;
(i | 0) < (count | 0);
p = (p + 8) | 0, i = (i + 1) | 0
) {
sum = (sum + arr[ p >> 3 ]) | 0;
}
return +(sum / count);
}
return {
foo: foo
};
}
var heap = new ArrayBuffer( 0x1000 );
var foo = fooASM( window, null, heap ).foo;
foo( 10, 20 ); // 233
```
注意:?這個asm.js例子是為了演示的目的手動編寫的,所以它與那些支持asm.js的編譯工具生產的代碼的表現不同。但是它展示了asm.js代碼的典型性質,特別是類型提示與為了臨時變量存儲而使用`heap`緩沖。
第一個`fooASM(..)`調用用它的`heap`分配區建立了我們的asm.js模塊。結果是一個我們可以調用任意多次的`foo(..)`函數。這些調用應當會被支持asm.js的JS引擎特別優化。重要的是,前面的代碼完全是標準JS,而且會在非asm.js引擎中工作的很好(但沒有特別優化)。
很明顯,使asm.js代碼可優化的各種限制降低了廣泛使用這種代碼的可能性。對于任意給出的JS程序,asm.js沒有必要為成為一個一般化的優化集合。相反,它的本意是提供針對一種處理特定任務——如密集數學操作(那些用于游戲中圖形處理的)——的優化方法。
## 復習
本書的前四章基于這樣的前提:異步編碼模式給了你編寫更高效代碼的能力,這通常是一個非常重要的改進。但是異步行為也就能幫你這么多,因為它在基礎上仍然使用一個單獨的事件輪詢線程。
所以在這一章我們涵蓋了幾種程序級別的機制來進一步提升性能。
Web Worker讓你在一個分離的線程上運行一個JS文件(也就是程序),使用異步事件在線程之間傳遞消息。對于將長時間運行或資源密集型任務掛載到一個不同線程,從而讓主UI線程保持相應來說,它們非常棒。
SIMD提議將CPU級別的并行數學操作映射到JavaScript API上來提供高性能數據并行操作,比如在大數據集合上進行數字處理。
最后,asm.js描述了一個JavaScript的小的子集,它回避了JS中不易優化的部分(比如垃圾回收與強制轉換)并讓JS引擎通過主動優化識別并運行這樣的代碼。asm.js可以手動編寫,但是極其麻煩且易錯,就像手動編寫匯編語言。相反,asm.js的主要意圖是作為一個從其他高度優化的程序語言交叉編譯來的目標——例如,Emscripten([https://github.com/kripken/emscripten/wiki)可以將C/C++轉譯為JavaScript。](https://github.com/kripken/emscripten/wiki%EF%BC%89%E5%8F%AF%E4%BB%A5%E5%B0%86C/C++%E8%BD%AC%E8%AF%91%E4%B8%BAJavaScript%E3%80%82)
雖然在本章沒有明確地提及,在很早以前的有關JavaScript的討論中存在著更激進的想法,包括近似地直接多線程功能(不僅僅是隱藏在數據結構API后面)。無論這是否會明確地發生,還是我們將看到更多并行機制偷偷潛入JS,但是在JS中發生更多程序級別優化的未來是可以確定的。