你懂 JavaScript 嗎?#26 程式效能(Program Performance)

你所不知道的 JS

本文主要會談到 web worker、SIMD 與 asm.js。

到目前為止我們只談論了如何有效的運用非同步處理模式,現在就來探討為什麼非同步處理對 JavaScript 來說這麼重要,其中最明顯的理由就是效能,本文主要探討的是更整體的程式層級的效能。

Web Worker

Web Worker 是屬於瀏覽器的新功能,而非 JavaScript 本身提供的功能,我們可以想像為 JavaScript 程式碼會在主執行緒(main thread)執行,並且又可以另闢一個戰場 worker thread 來提供 JavaScript 在背景執行,必要時再透過訊息溝通,這樣的好處是不阻塞 main thread 而可讓速度更快,比起目前的非同步的共時,效能更佳。

Web Workers

圖片來源:Browser Rendering Optimization - JavaScript

使用 Dedicated Web Worker 來做兩數相乘

從範例理解 web worker 到底是什麼和怎麼用吧 (๑•̀ㅂ•́)و✧

握手

由於 worker 是需要起 server 的,推薦使用 Web Server for Chrome 來執行範例網站。

這裡簡單使用 dedicated web worker 來做相乘運算,算完後再丟回 main thread,點這裡看完整程式碼,點那裡看 Demo。

兩數相乘範例

範例如下。

if (window.Worker) {
  var myWorker = new Worker('worker.js');

  first.onchange = function() {
    myWorker.postMessage([first.value,second.value]);
    console.log('Message posted to worker');
  };

  second.onchange = function() {
    myWorker.postMessage([first.value,second.value]);
    console.log('Message posted to worker');
  };

  myWorker.onmessage = function(e) {
    result.textContent = e.data;
    console.log('Message received from worker');
  };
}

說明

注意

使用 Shared Web Worker 來做兩數相乘

dedicated worker 使用的是獨立單一的 worker,若執行相同的任務還開獨立的 worker 就太浪費了,改用 shared worker 吧,這樣就可以共享資源了。

shared web worker 能夠被多個程式腳本存取,即使是跨越不同 window、iframe 或 worker。這個範例是使用同一個 worker 來做兩數相乘的運算,但可被不同的程式取用,分別達到兩數相乘(multiply.js)與計算平方(square.js)的功能,點這裡看完整程式碼,點那裡看 Demo。

兩數相乘。

兩數相乘範例

計算平方。

兩數相乘範例

shared worker 和 dedicated worker 作法類似,除了以下三個部份是不一樣的…

  1. 用 SharedWorker 建構子來產生 shared worker。
  2. 腳本可共用 shared worker 來做運算,例如,square.jsmultiply.js 共用 worker.js 來做兩數相乘的運算;而 dedicated worker 是個別程式單獨使用一個 worker。

  3. 與 shared worker 的溝通必須要透過 port 物件,其實 dedicated workers 也是如此,只不過一切是在背景自動完成。

以下是主程式的部分程式碼,用來將要做運算的數字丟給 worker 來計算。

squareNumber.onchange = function() {
  myWorker.port.postMessage([squareNumber.value, squareNumber.value]);
  console.log('Message posted to worker');
}

以下是 web workers 的部分程式碼。

onconnect = function(e) { // 監聽連線建立的 onconnect 事件,從 onconnect 取得 port 物件
  var port = e.ports[0];

  port.onmessage = function(e) {
    var workerResult = 'Result: ' + (e.data[0] * e.data[1]);
    port.postMessage(workerResult);
  }
}

瀏覽器支援度

主流瀏覽器皆支援。

Web Workers 瀏覽器支援度

圖片來源:Can I use…Web Workers

這麼好用的東西怎麼可以不支援!

YA

注意,shared web workers 目前只能用於 Chrome 66+、Firefox 60+,且手機幾乎不支援。

shared web workers

圖片來源:Can I use…Shared Web Workers

手機幾乎不支援???真是太糟糕惹

我覺得不行

Web Worker 的 Polyfill

本書作者推薦自己撰寫的 polyfill他覺得其他人寫得都不夠好

我覺得OK

SIMD

SIMD 是指「單指令流多資料流」(single instruction, multiple data),這是資料平行運算的一種形式,相較於 web worker 是針對程式邏輯區塊的平行運算而言,SIMD 更強調的是資料的位元能被平行處理。因此,SIMD 用的不是瀏覽器所提供的多執行緒的方式,而是 CPU 所提供的指令-短的向量型別與 API。

更多資訊可參考這裡

asm.js

asm.js 是用來指稱 JavaScript 中可高度最佳化的一個子集,藉由避開難以最佳化的部份,像是垃圾回收、強制轉型等,asm.js 的程式碼可被 JavaScript 引擎識別出來並特別關注,積極的進行最佳化。

那麼,該怎麼使用 asm.js 來進行最佳化呢?

asm.js 與型別強制轉型關係密切,這是因為 JavaScript 引擎花費了大量的功夫在追蹤變數與其所轉換的多種型別的值,這樣才能在必要時刻處理型別間的強制轉型。

因此,我們可以透過一些技巧來提示 JavaScript 引擎某些變數或作業預期得到什麼型別?讓它能跳過這些強制轉型的追蹤步驟。

例如…

var a = 42;
var b = a; // 這裡可能會進行型別轉換...

我們可利用 |(二進位的 OR)與 0 來確保 b 會被當成一個 32 位元的整數,而不需要追蹤強制轉型的步驟。

var a = 42;
var b = a | 0;

這樣能確保這絕對是 32 位元的整數加法。

(a + b) | 0;

除了型別與強制轉型外,另一個 JavaScript 引擎大量耗損效能的地方是在記憶體配置、垃圾處理與範疇的存取,對此 asm.js 來說,解法就是宣告一個 asm.js 模組,其中要傳入一個 stdlib 的命名空間來匯入所需符號,再來宣告一個 heap 作為記憶體的保留區域,這樣之後就不用再多要求記憶體或釋放已使用過的記憶體(就是一個先預支固定額度以免之後被打擾的概念)。

範例如下,定義一個函式 foo,它接受起始與結束的整數 x 和 y,並計算這範圍中兩兩相鄰的數值的乘積,最後將乘積相加再取平均。

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

嚇到了吧!

嚇到吃手手

由以上程式碼可知,這種撰寫方式雖然效能佳,但並不好閱讀與廣泛使用,它只能用於針對特殊任務做最佳化處理,像是大量的數學運算、遊戲中的圖形處理等。

回顧

看完這篇文章,我們到底有什麼收穫呢?藉由本文可以理解到…

References


同步發表於2019 鐵人賽


comments powered by Disqus