第五章: 程序性能

特別說明,為便于查閱,文章轉(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ī)制。

w1Worker對(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)化的未來是可以確定的枢贿。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末殉农,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子局荚,更是在濱河造成了極大的恐慌超凳,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,013評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件危队,死亡現(xiàn)場(chǎng)離奇詭異聪建,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)茫陆,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,205評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門金麸,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人簿盅,你說我怎么就攤上這事挥下。” “怎么了桨醋?”我有些...
    開封第一講書人閱讀 152,370評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵棚瘟,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我喜最,道長(zhǎng)偎蘸,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,168評(píng)論 1 278
  • 正文 為了忘掉前任瞬内,我火速辦了婚禮迷雪,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘虫蝶。我一直安慰自己章咧,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,153評(píng)論 5 371
  • 文/花漫 我一把揭開白布能真。 她就那樣靜靜地躺著赁严,像睡著了一般。 火紅的嫁衣襯著肌膚如雪粉铐。 梳的紋絲不亂的頭發(fā)上疼约,一...
    開封第一講書人閱讀 48,954評(píng)論 1 283
  • 那天,我揣著相機(jī)與錄音秦躯,去河邊找鬼忆谓。 笑死,一個(gè)胖子當(dāng)著我的面吹牛踱承,可吹牛的內(nèi)容都是我干的倡缠。 我是一名探鬼主播哨免,決...
    沈念sama閱讀 38,271評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼昙沦!你這毒婦竟也來了琢唾?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,916評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤盾饮,失蹤者是張志新(化名)和其女友劉穎采桃,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體丘损,經(jīng)...
    沈念sama閱讀 43,382評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡普办,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,877評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了徘钥。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片衔蹲。...
    茶點(diǎn)故事閱讀 37,989評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖呈础,靈堂內(nèi)的尸體忽然破棺而出舆驶,到底是詐尸還是另有隱情,我是刑警寧澤而钞,帶...
    沈念sama閱讀 33,624評(píng)論 4 322
  • 正文 年R本政府宣布沙廉,位于F島的核電站,受9級(jí)特大地震影響臼节,放射性物質(zhì)發(fā)生泄漏撬陵。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,209評(píng)論 3 307
  • 文/蒙蒙 一网缝、第九天 我趴在偏房一處隱蔽的房頂上張望袱结。 院中可真熱鬧,春花似錦途凫、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,199評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至促王,卻和暖如春犀盟,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背蝇狼。 一陣腳步聲響...
    開封第一講書人閱讀 31,418評(píng)論 1 260
  • 我被黑心中介騙來泰國打工阅畴, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人迅耘。 一個(gè)月前我還...
    沈念sama閱讀 45,401評(píng)論 2 352
  • 正文 我出身青樓贱枣,卻偏偏與公主長(zhǎng)得像监署,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子纽哥,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,700評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容

  • 官方中文版原文鏈接 感謝社區(qū)中各位的大力支持钠乏,譯者再次奉上一點(diǎn)點(diǎn)福利:阿里云產(chǎn)品券,享受所有官網(wǎng)優(yōu)惠春塌,并抽取幸運(yùn)大...
    HetfieldJoe閱讀 2,322評(píng)論 1 21
  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 171,498評(píng)論 25 707
  • 1. 需求 Java開發(fā)使用Oracle晓避,所以需要使用Oracle的JDBC驅(qū)動(dòng),而Maven中心庫沒有Oracl...
    羽落莫云閱讀 10,716評(píng)論 2 7
  • 今天是學(xué)校里每星期都有的大掃除只壳,這次輪到我和另外一個(gè)室友一起打掃寢室衛(wèi)生俏拱,我掃地,她拖地吼句,快搞定的時(shí)候有一個(gè)室友...
    貪婪魘閱讀 139評(píng)論 0 0
  • 【營銷特訓(xùn)營成長(zhǎng)日記第5天】 今天又有了新的收獲锅必,新的驚喜!學(xué)會(huì)了和不同的人說話用不同的方式與話術(shù)命辖!每天都在努力况毅,...
    琛琛寶貝閱讀 154評(píng)論 0 0