特別說明,為便于查閱,文章轉(zhuǎn)自https://github.com/getify/You-Dont-Know-JS
這本書至此一直是關(guān)于如何更有效地利用異步模式吉挣。但是我們還沒有直接解釋為什么異步對(duì)于JS如此重要盛正。最明顯明確的理由就是 性能。
舉個(gè)例子杖小,如果你要發(fā)起兩個(gè)Ajax請(qǐng)求,而且他們是相互獨(dú)立的愚墓,但你在進(jìn)行下一個(gè)任務(wù)之前需要等到他們?nèi)客瓿捎枞ǎ憔陀袃煞N選擇來對(duì)這種互動(dòng)建立模型:順序和并發(fā)。
你可以發(fā)起第一個(gè)請(qǐng)求并等到它完成再發(fā)起第二個(gè)請(qǐng)求浪册∩ㄏ伲或者,就像我們?cè)趐romise和generator中看到的那樣村象,你可以“并列地”發(fā)起兩個(gè)請(qǐng)求笆环,并在繼續(xù)下一步之前讓一個(gè)“門”等待它們?nèi)客瓿伞?/p>
顯然,后者要比前者性能更好厚者。而更好的性能一般都會(huì)帶來更好的用戶體驗(yàn)躁劣。
異步(并發(fā)穿插)甚至可能僅僅增強(qiáng)高性能的印象,即便整個(gè)程序依然要用相同的時(shí)間才成完成库菲。用戶對(duì)性能的印象意味著一切——如果不能再多的話账忘!——和實(shí)際可測(cè)量的性能一樣重要。
現(xiàn)在熙宇,我們想超越局部的異步模式鳖擒,轉(zhuǎn)而在程序級(jí)別的水平上討論一些宏觀的性能細(xì)節(jié)。
注意: 你可能會(huì)想知道關(guān)于微性能問題烫止,比如a++
與++a
哪個(gè)更快蒋荚。我們會(huì)在下一章“基準(zhǔn)分析與調(diào)優(yōu)”中討論這類性能細(xì)節(jié)。
Web Workers
如果你有一些處理密集型的任務(wù)馆蠕,但你不想讓它們?cè)谥骶€程上運(yùn)行(那樣會(huì)使瀏覽器/UI變慢)期升,你可能會(huì)希望JavaScript可以以多線程的方式操作惊奇。
在第一章中,我們?cè)敿?xì)地談到了關(guān)于JavaScript如何是單線程的吓妆。那仍然是成立的赊时。但是單線程不是組織你程序運(yùn)行的唯一方法。
想象將你的程序分割成兩塊兒行拢,在UI主線程上運(yùn)行其中的一塊兒祖秒,而在一個(gè)完全分離的線程上運(yùn)行另一塊兒。
這樣的結(jié)構(gòu)會(huì)引發(fā)什么我們需要關(guān)心的問題舟奠?
其一竭缝,你會(huì)想知道運(yùn)行在一個(gè)分離的線程上是否意味著它在并行運(yùn)行(在多CPU/內(nèi)核的系統(tǒng)上),如此在第二個(gè)線程上長(zhǎng)時(shí)間運(yùn)行的處理將 不會(huì) 阻塞主程序線程沼瘫。否則抬纸,“虛擬線程”所帶來的好處,不會(huì)比我們已經(jīng)在異步并發(fā)的JS中得到的更多耿戚。
而且你會(huì)想知道這兩塊兒程序是否訪問共享的作用域/資源湿故。如果是,那么你就要對(duì)付多線程語言(Java膜蛔,C++等等)的所有問題坛猪,比如協(xié)作式或搶占式鎖定(互斥,等)皂股。這是很多額外的工作墅茉,而且不應(yīng)當(dāng)輕易著手。
換一個(gè)角度呜呐,如果這兩塊兒程序不能共享作用域/資源就斤,你會(huì)想知道它們將如何“通信”。
所有這些我們需要考慮的問題蘑辑,指引我們探索一個(gè)在近HTML5時(shí)代被加入web平臺(tái)的特性洋机,稱為“Web Worker”。這是一個(gè)瀏覽器(也就是宿主環(huán)境)特性以躯,而且?guī)缀鹾蚃S語言本身沒有任何關(guān)系槐秧。也就是說,JavaScript 當(dāng)前 并沒有任何特性可以支持多線程運(yùn)行忧设。
但是一個(gè)像你的瀏覽器那樣的環(huán)境可以很容易地提供多個(gè)JavaScript引擎實(shí)例,每個(gè)都在自己的線程上颠通,并允許你在每個(gè)線程上運(yùn)行不同的程序址晕。你的程序中分離的線程塊兒中的每一個(gè)都稱為一個(gè)“(Web)Worker”。這種并行機(jī)制叫做“任務(wù)并行機(jī)制”顿锰,它強(qiáng)調(diào)將你的程序分割成塊兒來并行運(yùn)行谨垃。
在你的主JS程序(或另一個(gè)Worker)中启搂,你可以這樣初始化一個(gè)Worker:
var w1 = new Worker( "http://some.url.1/mycoolworker.js" );
這個(gè)URL應(yīng)當(dāng)指向JS文件的位置(不是一個(gè)HTML網(wǎng)頁!)刘陶,它將會(huì)被加載到一個(gè)Worker胳赌。然后瀏覽器會(huì)啟動(dòng)一個(gè)分離的線程,讓這個(gè)文件在這個(gè)線程上作為獨(dú)立的程序運(yùn)行匙隔。
注意: 這種用這樣的URL創(chuàng)建的Worker稱為“專用(Dedicated)Wroker”疑苫。但與提供一個(gè)外部文件的URL不同的是,你也可以通過提供一個(gè)Blob URL(另一個(gè)HTML5特性)來創(chuàng)建一個(gè)“內(nèi)聯(lián)(Inline)Worker”纷责;它實(shí)質(zhì)上是一個(gè)存儲(chǔ)在單一(二進(jìn)制)值中的內(nèi)聯(lián)文件捍掺。但是,Blob超出了我們要在這里討論的范圍再膳。
Worker不會(huì)相互挺勿,或者與主程序共享任何作用域或資源——那會(huì)將所有的多線程編程的噩夢(mèng)帶到我們面前——取而代之的是一種連接它們的基本事件消息機(jī)制。
w1
Worker對(duì)象是一個(gè)事件監(jiān)聽器和觸發(fā)器喂柒,它允許你監(jiān)聽Worker發(fā)出的事件也允許你向Worker發(fā)送事件不瓶。
這是如何監(jiān)聽事件(實(shí)際上,是固定的"message"
事件):
w1.addEventListener( "message", function(evt){
// evt.data
} );
而且你可以發(fā)送"message"
事件給Worker:
w1.postMessage( "something cool to say" );
在Worker內(nèi)部灾杰,消息是完全對(duì)稱的:
// "mycoolworker.js"
addEventListener( "message", function(evt){
// evt.data
} );
postMessage( "a really cool reply" );
要注意的是蚊丐,一個(gè)專用Worker與它創(chuàng)建的程序是一對(duì)一的關(guān)系。也就是吭露,"message"
事件不需要消除任何歧義吠撮,因?yàn)槲覀兛梢源_定它只可能來自于這種一對(duì)一關(guān)系——不是從Wroker來的,就是從主頁面來的讲竿。
通常主頁面的程序會(huì)創(chuàng)建Worker泥兰,但是一個(gè)Worker可以根據(jù)需要初始化它自己的子Worker——稱為subworker。有時(shí)將這樣的細(xì)節(jié)委托給一個(gè)“主”Worker十分有用题禀,它可以生成其他Worker來處理任務(wù)的一部分鞋诗。不幸的是,在本書寫作的時(shí)候迈嘹,Chrome還沒有支持subworker削彬,然而Firefox支持。
要從創(chuàng)建一個(gè)Worker的程序中立即殺死它秀仲,可以在Worker對(duì)象(就像前一個(gè)代碼段中的w1
)上調(diào)用terminate()
融痛。突然終結(jié)一個(gè)Worker線程不會(huì)給它任何機(jī)會(huì)結(jié)束它的工作,或清理任何資源神僵。這和你關(guān)閉瀏覽器的標(biāo)簽頁來殺死一個(gè)頁面相似雁刷。
如果你在瀏覽器中有兩個(gè)或多個(gè)頁面(或者打開同一個(gè)頁面的多個(gè)標(biāo)簽頁!)保礼,試著從同一個(gè)文件URL中創(chuàng)建Worker沛励,實(shí)際上最終結(jié)果是完全分離的Worker责语。待一會(huì)兒我們就會(huì)討論“共享”Worker的方法。
注意: 看起來一個(gè)惡意的或者是呆頭呆腦的JS程序可以很容易地通過在系統(tǒng)上生成數(shù)百個(gè)Worker來發(fā)起拒絕服務(wù)攻擊(Dos攻擊)目派,看起來每個(gè)Worker都在自己的線程上坤候。雖然一個(gè)Worker將會(huì)在存在于一個(gè)分離的線程上是有某種保證的,但這種保證不是沒有限制的企蹭。系統(tǒng)可以自由決定有多少實(shí)際的線程/CPU/內(nèi)核要去創(chuàng)建白筹。沒有辦法預(yù)測(cè)或保證你能訪問多少,雖然很多人假定它至少和可用的CPU/內(nèi)核數(shù)一樣多练对。我認(rèn)為最安全的臆測(cè)是遍蟋,除了主UI線程外至少有一個(gè)線程,僅此而已螟凭。
Worker 環(huán)境
在Worker內(nèi)部虚青,你不能訪問主程序的任何資源。這意味著你不能訪問它的任何全局變量螺男,你也不能訪問頁面的DOM或其他資源棒厘。記住:它是一個(gè)完全分離的線程下隧。
然而奢人,你可以實(shí)施網(wǎng)絡(luò)操作(Ajax,WebSocket)和設(shè)置定時(shí)器淆院。另外何乎,Worker可以訪問它自己的幾個(gè)重要全局變量/特性的拷貝,包括navigator
土辩,location
支救,JSON
,和applicationCache
拷淘。
你還可以使用importScripts(..)
加載額外的JS腳本到你的Worker中:
// 在Worker內(nèi)部
importScripts( "foo.js", "bar.js" );
這些腳本會(huì)被同步地加載各墨,這意味著在文件完成加載和運(yùn)行之前,importScripts(..)
調(diào)用會(huì)阻塞Worker的執(zhí)行启涯。
注意: 還有一些關(guān)于暴露<canvas>
API給Worker的討論贬堵,其中包括使canvas成為Transferable的(見“數(shù)據(jù)傳送”一節(jié)),這將允許Worker來實(shí)施一些精細(xì)的脫線程圖形處理结洼,在高性能的游戲(WebGL)和其他類似應(yīng)用中可能很有用黎做。雖然這在任何瀏覽器中都還不存在,但是很有可能在近未來發(fā)生松忍。
Web Worker的常見用途是什么引几?
- 處理密集型的數(shù)學(xué)計(jì)算
- 大數(shù)據(jù)集合的排序
- 數(shù)據(jù)操作(壓縮,音頻分析挽铁,圖像像素操作等等)
- 高流量網(wǎng)絡(luò)通信
數(shù)據(jù)傳送
你可能注意到了這些用途中的大多數(shù)的一個(gè)共同性質(zhì)伟桅,就是它們要求使用事件機(jī)制穿越線程間的壁壘來傳遞大量的信息,也許是雙向的叽掘。
在Worker的早期楣铁,將所有數(shù)據(jù)序列化為字符串是唯一的選擇。除了在兩個(gè)方向上進(jìn)行序列化時(shí)速度上變慢了更扁,另外一個(gè)主要缺點(diǎn)是盖腕,數(shù)據(jù)是被拷貝的,這意味著內(nèi)存用量翻了一倍(以及在后續(xù)垃圾回收上的流失)浓镜。
謝天謝地溃列,現(xiàn)在我們有了幾個(gè)更好的選擇。
如果你傳遞一個(gè)對(duì)象膛薛,在另一端一個(gè)所謂的“結(jié)構(gòu)化克隆算法(Structured Cloning Algorithm)”(https://developer.mozilla.org/en-US/docs/Web/Guide/API/DOM/The_structured_clone_algorithm)會(huì)用于拷貝/復(fù)制這個(gè)對(duì)象听隐。這個(gè)算法相當(dāng)精巧,甚至可以處理帶有循環(huán)引用的對(duì)象復(fù)制哄啄。to-string/from-string的性能劣化沒有了雅任,但用這種方式我們依然面對(duì)著內(nèi)存用量的翻倍。IE10以上版本咨跌,和其他主流瀏覽器都對(duì)此有支持沪么。
一個(gè)更好的選擇,特別是對(duì)大的數(shù)據(jù)集合而言锌半,是“Transferable對(duì)象”(http://updates.html5rocks.com/2011/12/Transferable-Objects-Lightning-Fast)禽车。它使對(duì)象的“所有權(quán)”被傳送,而對(duì)象本身沒動(dòng)刊殉。一旦你傳送一個(gè)對(duì)象給Worker殉摔,它在原來的位置就空了出來或者不可訪問——這消除了共享作用域的多線程編程中的災(zāi)難。當(dāng)然冗澈,所有權(quán)的傳送可以雙向進(jìn)行钦勘。
選擇使用Transferable對(duì)象不需要你做太多;任何實(shí)現(xiàn)了Transferable接口(https://developer.mozilla.org/en-US/docs/Web/API/Transferable)的數(shù)據(jù)結(jié)構(gòu)都將自動(dòng)地以這種方式傳遞(Firefox和Chrome支持此特性)亚亲。
舉個(gè)例子彻采,有類型的數(shù)組如Uint8Array
(見本系列的 ES6與未來)是一個(gè)“Transferables”。這是你如何用postMessage(..)
來傳送一個(gè)Transferable對(duì)象:
// `foo` 是一個(gè) `Uint8Array`
postMessage( foo.buffer, [ foo.buffer ] );
第一個(gè)參數(shù)是未經(jīng)加工的緩沖捌归,而第二個(gè)參數(shù)是要傳送的內(nèi)容的列表肛响。
不支持Transferable對(duì)象的瀏覽器簡(jiǎn)單地降級(jí)到結(jié)構(gòu)化克隆,這意味著性能上的降低惜索,而不是徹底的特性失靈特笋。
共享的Workers
如果你的網(wǎng)站或應(yīng)用允許多個(gè)標(biāo)簽頁加載同一個(gè)網(wǎng)頁(一個(gè)常見的特性),你也許非常想通過防止復(fù)制專用Worker來降低系統(tǒng)資源的使用量巾兆;這方面最常見的資源限制是網(wǎng)絡(luò)套接字鏈接猎物,因?yàn)闉g覽器限制同時(shí)連接到一個(gè)服務(wù)器的連接數(shù)量虎囚。當(dāng)然,限制從客戶端來的鏈接數(shù)也緩和了你的服務(wù)器資源需求蔫磨。
在這種情況下淘讥,創(chuàng)建一個(gè)單獨(dú)的中心化Worker,讓你的網(wǎng)站或應(yīng)用的所有網(wǎng)頁實(shí)例可以 共享 它是十分有用的堤如。
這稱為SharedWorker
蒲列,你會(huì)這樣創(chuàng)建它(僅有Firefox與Chrome支持此特性):
var w1 = new SharedWorker( "http://some.url.1/mycoolworker.js" );
因?yàn)橐粋€(gè)共享Worker可以連接或被連接到你的網(wǎng)站上的多個(gè)程序?qū)嵗蚓W(wǎng)頁,Worker需要一個(gè)方法來知道消息來自哪個(gè)程序搀罢。這種唯一的標(biāo)識(shí)稱為“端口(port)”——聯(lián)想網(wǎng)絡(luò)套接字端口蝗岖。所以調(diào)用端程序必須使用Worker的port
對(duì)象來通信:
w1.port.addEventListener( "message", handleMessages );
// ..
w1.port.postMessage( "something cool" );
另外,端口連接必須被初始化榔至,就像這樣:
w1.port.start();
在共享Worker內(nèi)部抵赢,一個(gè)額外的事件必須被處理:"connect"
。這個(gè)事件為這個(gè)特定的連接提供端口object
洛退。保持多個(gè)分離的連接最簡(jiǎn)單的方法是在port
上使用閉包瓣俯,就像下面展示的那樣,同時(shí)在"connect"
事件的處理器內(nèi)部定義這個(gè)連接的事件監(jiān)聽與傳送:
// 在共享Worker的內(nèi)部
addEventListener( "connect", function(evt){
// 為這個(gè)連接分配的端口
var port = evt.ports[0];
port.addEventListener( "message", function(evt){
// ..
port.postMessage( .. );
// ..
} );
// 初始化端口連接
port.start();
} );
除了這點(diǎn)不同兵怯,共享與專用Worker的功能和語義是一樣的彩匕。
注意: 如果在一個(gè)端口的連接終結(jié)時(shí)還有其他端口的連接存活著的話,共享Worker也會(huì)存活下來媒区,而專用Worker會(huì)在與初始化它的程序間接終結(jié)時(shí)終結(jié)驼仪。
填補(bǔ) Web Workers
對(duì)于并行運(yùn)行的JS程序在性能考量上,Web Worker十分吸引人袜漩。然而绪爸,你的代碼可能運(yùn)行在對(duì)此缺乏支持的老版本瀏覽器上薄啥。因?yàn)閃orker是一個(gè)API而不是語法面哼,所以在某種程度上它們可以被填補(bǔ)拍顷。
如果瀏覽器不支持Worker派阱,那就根本沒有辦法從性能的角度來模擬多線程。Iframe通常被認(rèn)為可以提供并行環(huán)境嚣州,但在所有的現(xiàn)代瀏覽器中它們實(shí)際上和主頁運(yùn)行在同一個(gè)線程上畸肆,所以用它們來模擬并行機(jī)制是不夠的佑惠。
正如我們?cè)诘谝徽轮性敿?xì)討論的溢陪,JS的異步能力(不是并行機(jī)制)來自于事件輪詢隊(duì)列萍虽,所以你可以用計(jì)時(shí)器(setTimeout(..)
等等)來強(qiáng)制模擬的Worker是異步的。然后你只需要提供Worker API的填補(bǔ)就行了形真。這里有一份列表(https://github.com/Modernizr/Modernizr/wiki/HTML5-Cross-Browser-Polyfills#web-workers)杉编,但坦白地說它們看起來都不怎么樣。
我在這里(https://gist.github.com/getify/1b26accb1a09aa53ad25)寫了一個(gè)填補(bǔ)Worker
的輪廓。它很基礎(chǔ)邓馒,但應(yīng)該滿足了簡(jiǎn)單的Worker
支持嘶朱,它的雙向信息傳遞可以正確工作,還有"onerror"
處理绒净。你可能會(huì)擴(kuò)展它來支持更多特性见咒,比如terminate()
或模擬共享Worker,只要你覺得合適挂疆。
注意: 你不能模擬同步阻塞,所以這個(gè)填補(bǔ)不允許使用importScripts(..)
下翎。另一個(gè)選擇可能是轉(zhuǎn)換并傳遞Worker的代碼(一旦Ajax加載后)缤言,來重寫一個(gè)importScripts(..)
填補(bǔ)的一些異步形式,也許使用一個(gè)promise相關(guān)的接口视事。
SIMD
一個(gè)指令胆萧,多個(gè)數(shù)據(jù)(SIMD)是一種“數(shù)據(jù)并行機(jī)制”形式,與Web Worker的“任務(wù)并行機(jī)制”相對(duì)應(yīng)俐东,因?yàn)樗麖?qiáng)調(diào)的不是程序邏輯的塊兒被并行化跌穗,而是多個(gè)字節(jié)的數(shù)據(jù)被并行地處理。
使用SIMD虏辫,線程不提供并行機(jī)制蚌吸。相反,現(xiàn)代CPU用數(shù)字的“向量”提供SIMD能力——想想:指定類型的數(shù)組——還有可以在所有這些數(shù)字上并行操作的指令砌庄;這些是利用底層操作的指令級(jí)別的并行機(jī)制羹唠。
使SIMD能力包含在JavaScript中的努力主要是由Intel帶頭的(https://01.org/node/1495),名義上是Mohammad Haghighat(在本書寫作的時(shí)候)娄昆,與Firefox和Chrome團(tuán)隊(duì)合作佩微。SIMD處于早期標(biāo)準(zhǔn)化階段,而且很有可能被加入未來版本的JavaScript中萌焰,很可能在ES7的時(shí)間框架內(nèi)哺眯。
SIMD JavaScript提議向JS代碼暴露短向量類型與API,它們?cè)赟IMD可用的系統(tǒng)中將操作直接映射為CPU指令的等價(jià)物扒俯,同時(shí)在非SIMD系統(tǒng)中退回到非并行化操作的“shim”奶卓。
對(duì)于數(shù)據(jù)密集型的應(yīng)用程序(信號(hào)分析,對(duì)圖形的矩陣操作等等)來說陵珍,這種并行數(shù)學(xué)處理在性能上的優(yōu)勢(shì)是十分明顯的寝杖!
在本書寫作時(shí),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 ]
這里展示了兩種不同的向量數(shù)據(jù)類型互纯,32位浮點(diǎn)數(shù)和32位整數(shù)瑟幕。你可以看到這些向量正好被設(shè)置為4個(gè)32位元素,這與大多數(shù)CPU中可用的SIMD向量的大小(128位)相匹配只盹。在未來我們看到一個(gè)x8
(或更大@蓖)版本的這些API也是可能的。
除了mul()
和add()
殖卑,許多其他操作也很可能被加入站削,比如sub()
,div()
孵稽,abs()
许起,neg()
,sqrt()
菩鲜,reciprocal()
园细,reciprocalSqrt()
(算數(shù)運(yùn)算),shuffle()
(重拍向量元素)接校,and()
猛频,or()
,xor()
蛛勉,not()
(邏輯運(yùn)算)鹿寻,equal()
,greaterThan()
诽凌,lessThan()
(比較運(yùn)算)毡熏,shiftLeft()
,shiftRightLogical()
皿淋,shiftRightArithmetic()
(輪換)招刹,fromFloat32x4()
,和fromInt32x4()
(變換)窝趣。
注意: 這里有一個(gè)SIMD功能的官方“填補(bǔ)”(很有希望疯暑,預(yù)期的,著眼未來的填補(bǔ))(https://github.com/johnmccutchan/ecmascript_simd)哑舒,它描述了許多比我們?cè)谶@一節(jié)中沒有講到的許多計(jì)劃中的SIMD功能妇拯。
asm.js
“asm.js”(http://asmjs.org/)是可以被高度優(yōu)化的JavaScript語言子集的標(biāo)志。通過小心地回避那些特定的很難優(yōu)化的(垃圾回收洗鸵,強(qiáng)制轉(zhuǎn)換越锈,等等)機(jī)制和模式,asm.js風(fēng)格的代碼可以被JS引擎識(shí)別膘滨,而且用主動(dòng)地底層優(yōu)化進(jìn)行特殊的處理甘凭。
與本章中討論的其他性能優(yōu)化機(jī)制不同的是,asm.js沒必須要是必須被JS語言規(guī)范所采納的東西火邓。確實(shí)有一個(gè)asm.js規(guī)范(http://asmjs.org/spec/latest/)丹弱,但它主要是追蹤一組關(guān)于優(yōu)化的候選對(duì)象的推論德撬,而不是JS引擎的需求。
目前還沒有新的語法被提案躲胳。取而代之的是蜓洪,ams.js建議了一些方法,用來識(shí)別那些符合ams.js規(guī)則的既存標(biāo)準(zhǔn)JS語法坯苹,并且讓引擎相應(yīng)地實(shí)現(xiàn)它們自己的優(yōu)化功能隆檀。
關(guān)于ams.js應(yīng)當(dāng)如何在程序中活動(dòng)的問題,在瀏覽器生產(chǎn)商之間存在一些爭(zhēng)議粹湃。早期版本的asm.js實(shí)驗(yàn)中恐仑,要求一個(gè)"use asm";
編譯附注(與strict模式的"use strict";
類似)來幫助JS引擎來尋找asm.js優(yōu)化的機(jī)會(huì)和提示。另一些人則斷言asm.js應(yīng)當(dāng)只是一組啟發(fā)式算法再芋,讓引擎自動(dòng)地識(shí)別而不用作者做任何額外的事情菊霜,這意味著理論上既存的程序可以在不用做任何特殊的事情的情況下從asm.js優(yōu)化中獲益。
如何使用 asm.js 進(jìn)行優(yōu)化
關(guān)于asm.js需要理解的第一件事情是類型和強(qiáng)制轉(zhuǎn)換济赎。如果JS引擎不得不在變量的操作期間一直追蹤一個(gè)變量?jī)?nèi)的值的類型,以便于在必要時(shí)它可以處理強(qiáng)制轉(zhuǎn)換记某,那么就會(huì)有許多額外的工作使程序處于次優(yōu)化狀態(tài)司训。
注意: 為了說明的目的,我們將在這里使用ams.js風(fēng)格的代碼液南,但要意識(shí)到的是你手寫這些代碼的情況不是很常見壳猜。asm.js的本意更多的是作為其他工具的編譯目標(biāo),比如Emscripten(https://github.com/kripken/emscripten/wiki)滑凉。當(dāng)然你寫自己的asm.js代碼也是可能的统扳,但是這通常不是一個(gè)好主意,因?yàn)槟菢拥拇a非常底層畅姊,而這意味著它會(huì)非常耗時(shí)而且易錯(cuò)咒钟。盡管如此,也會(huì)有情況使你想要為了ams.js優(yōu)化的目的手動(dòng)調(diào)整代碼若未。
這里有一些“技巧”朱嘴,你可以使用它們來提示支持asm.js的JS引擎變量/操作預(yù)期的類型是什么,以便于它可以跳過那些強(qiáng)制轉(zhuǎn)換追蹤的步驟粗合。
舉個(gè)例子:
var a = 42;
// ..
var b = a;
在這個(gè)程序中萍嬉,賦值b = a
在變量中留下了類型分歧的問題。然而隙疚,它可以寫成這樣:
var a = 42;
// ..
var b = a | 0;
這里壤追,我們與值0
一起使用了|
(“二進(jìn)制或”),雖然它對(duì)值沒有任何影響供屉,但它確保這個(gè)值是一個(gè)32位整數(shù)行冰。這段代碼在普通的JS引擎中可以工作溺蕉,但是當(dāng)它運(yùn)行在支持asm.js的JS引擎上時(shí),它 可以 表示b
應(yīng)當(dāng)總是被作為32位整數(shù)來對(duì)待资柔,所以強(qiáng)制轉(zhuǎn)換追蹤可以被跳過焙贷。
類似地,兩個(gè)變量之間的加法操作可以被限定為性能更好的整數(shù)加法(而不是浮點(diǎn)數(shù)):
(a + b) | 0
再一次贿堰,支持asm.js的JS引擎可以看到這個(gè)提示辙芍,并推斷+
操作應(yīng)當(dāng)是一個(gè)32位整數(shù)加法,因?yàn)椴徽撛鯓诱麄€(gè)表達(dá)式的最終結(jié)果都將自動(dòng)是32位整數(shù)羹与。
asm.js 模塊
在JS中最托性能后腿的東西之一是關(guān)于內(nèi)存分配故硅,垃圾回收,與作用域訪問纵搁。asm.js對(duì)于這些問題建一個(gè)的一個(gè)方法是吃衅,聲明一個(gè)更加正式的asm.js“模塊”——不要和ES6模塊搞混;參見本系列的 ES6與未來腾誉。
對(duì)于一個(gè)asm.js模塊徘层,你需要明確傳入一個(gè)被嚴(yán)格遵循的名稱空間——在規(guī)范中以stdlib
引用,因?yàn)樗鼞?yīng)當(dāng)代表需要的標(biāo)準(zhǔn)庫——來引入需要的符號(hào)利职,而不是通過詞法作用域來使用全局對(duì)象趣效。在最基本的情況下,window
對(duì)象就是一個(gè)可接受的用于asm.js模塊的stdlib
對(duì)象猪贪,但是你可能應(yīng)該構(gòu)建一個(gè)更加被嚴(yán)格限制的對(duì)象跷敬。
你還必須定義一個(gè)“堆(heap)”——這只是一個(gè)別致的詞匯,它表示在內(nèi)存中被保留的位置热押,變量不必要求內(nèi)存分配或釋放已使用內(nèi)存就可以使用——并將它傳入西傀,這樣asm.js模塊就不必做任何導(dǎo)致內(nèi)存流失的的事情;它可以使用提前保留的空間桶癣。
一個(gè)“堆”就像一個(gè)有類型的ArrayBuffer
拥褂,比如:
var heap = new ArrayBuffer( 0x10000 ); // 64k 的堆
使用這個(gè)提前保留的64k的二進(jìn)制空間,一個(gè)asm.js模塊可以在這個(gè)緩沖區(qū)中存儲(chǔ)或讀取值鬼廓,而不受任何內(nèi)存分配與垃圾回收的性能損耗肿仑。比如,heap
緩沖區(qū)可以在模塊內(nèi)部用于備份一個(gè)64位浮點(diǎn)數(shù)值的數(shù)組碎税,像這樣:
var arr = new Float64Array( heap );
好了尤慰,讓我制作一個(gè)asm.js風(fēng)格模塊的快速,愚蠢的例子來描述這些東西是如何聯(lián)系在一起的雷蹂。我們將定義一個(gè)foo(..)
伟端,它為一個(gè)范圍接收一個(gè)開始位置(x
)和一個(gè)終止位置(y
),并且計(jì)算這個(gè)范圍內(nèi)所有相鄰的數(shù)字的積匪煌,然后最終計(jì)算這些值的平均值:
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;
// 計(jì)算范圍內(nèi)所有相鄰的數(shù)字的積
for (i = x | 0;
(i | 0) < (y | 0);
p = (p + 8) | 0, i = (i + 1) | 0
) {
// 存儲(chǔ)結(jié)果
arr[ p >> 3 ] = (i * (i + 1)) | 0;
}
// 計(jì)算所有中間值的平均值
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
注意: 這個(gè)asm.js例子是為了演示的目的手動(dòng)編寫的责蝠,所以它與那些支持asm.js的編譯工具生產(chǎn)的代碼的表現(xiàn)不同党巾。但是它展示了asm.js代碼的典型性質(zhì),特別是類型提示與為了臨時(shí)變量存儲(chǔ)而使用heap
緩沖霜医。
第一個(gè)fooASM(..)
調(diào)用用它的heap
分配區(qū)建立了我們的asm.js模塊齿拂。結(jié)果是一個(gè)我們可以調(diào)用任意多次的foo(..)
函數(shù)。這些調(diào)用應(yīng)當(dāng)會(huì)被支持asm.js的JS引擎特別優(yōu)化肴敛。重要的是署海,前面的代碼完全是標(biāo)準(zhǔn)JS,而且會(huì)在非asm.js引擎中工作的很好(但沒有特別優(yōu)化)医男。
很明顯砸狞,使asm.js代碼可優(yōu)化的各種限制降低了廣泛使用這種代碼的可能性。對(duì)于任意給出的JS程序镀梭,asm.js沒有必要為成為一個(gè)一般化的優(yōu)化集合刀森。相反,它的本意是提供針對(duì)一種處理特定任務(wù)——如密集數(shù)學(xué)操作(那些用于游戲中圖形處理的)——的優(yōu)化方法报账。
復(fù)習(xí)
本書的前四章基于這樣的前提:異步編碼模式給了你編寫更高效代碼的能力研底,這通常是一個(gè)非常重要的改進(jìn)。但是異步行為也就能幫你這么多透罢,因?yàn)樗诨A(chǔ)上仍然使用一個(gè)單獨(dú)的事件輪詢線程飘哨。
所以在這一章我們涵蓋了幾種程序級(jí)別的機(jī)制來進(jìn)一步提升性能。
Web Worker讓你在一個(gè)分離的線程上運(yùn)行一個(gè)JS文件(也就是程序)琐凭,使用異步事件在線程之間傳遞消息。對(duì)于將長(zhǎng)時(shí)間運(yùn)行或資源密集型任務(wù)掛載到一個(gè)不同線程浊服,從而讓主UI線程保持相應(yīng)來說统屈,它們非常棒。
SIMD提議將CPU級(jí)別的并行數(shù)學(xué)操作映射到JavaScript API上來提供高性能數(shù)據(jù)并行操作牙躺,比如在大數(shù)據(jù)集合上進(jìn)行數(shù)字處理愁憔。
最后,asm.js描述了一個(gè)JavaScript的小的子集孽拷,它回避了JS中不易優(yōu)化的部分(比如垃圾回收與強(qiáng)制轉(zhuǎn)換)并讓JS引擎通過主動(dòng)優(yōu)化識(shí)別并運(yùn)行這樣的代碼吨掌。asm.js可以手動(dòng)編寫,但是極其麻煩且易錯(cuò)脓恕,就像手動(dòng)編寫匯編語言膜宋。相反,asm.js的主要意圖是作為一個(gè)從其他高度優(yōu)化的程序語言交叉編譯來的目標(biāo)——例如炼幔,Emscripten(https://github.com/kripken/emscripten/wiki)可以將C/C++轉(zhuǎn)譯為JavaScript秋茫。
雖然在本章沒有明確地提及,在很早以前的有關(guān)JavaScript的討論中存在著更激進(jìn)的想法乃秀,包括近似地直接多線程功能(不僅僅是隱藏在數(shù)據(jù)結(jié)構(gòu)API后面)肛著。無論這是否會(huì)明確地發(fā)生圆兵,還是我們將看到更多并行機(jī)制偷偷潛入JS,但是在JS中發(fā)生更多程序級(jí)別優(yōu)化的未來是可以確定的枢贿。