WebAssembly

Webassembly(WASM)和CSS的Grid布局一樣都是一個新東西,Chrome從57開始支持。在講wasm之前我們先看代碼是怎么編譯的成機器碼,因為計算機只認識機器碼。

1. 機器碼

計算機只能運行機器碼,機器碼是一串二進制的數(shù)字,如下面的可執(zhí)行文件a.out:

上面顯示成16進制,是為了節(jié)省空間。

例如我用C寫一個函數(shù),如下:

int main(){
    int a = 5;
    int b = 6;
    int c = a + b;
    return 0;
}

然后把它編譯成一個可執(zhí)行文件,就變成了上面的a.out。a.out是一條條的指令組成的,如下圖所示,研究一下為了做一個加法是怎么進行的:

第一個字節(jié)表示它是哪條指令,每條指令的長度可能不一樣。上面總共有四條指令,第一條指令的意思是把0x5即5這個數(shù)放到內(nèi)存內(nèi)置為[rbp - 0x8]的位置,第二條指令的意思是把6放到內(nèi)存地址為[rbp - 0xc]的位置,為什么內(nèi)存的位置是這樣呢,因為我們定義了兩個局部變量a和b,局部變量是放在棧里面的,而new出來的是放在內(nèi)存堆里面的。上面main函數(shù)的內(nèi)存棧空間如下所示:

rbp是一個base pointer,即當前棧的基地址,這里應該為main函數(shù)入口地地址,然后又定義了兩個局部變量,它們依次入棧,棧由下往上增長,向內(nèi)存的低位增長,在我的這個Linux操作系統(tǒng)上是這樣的。最后return返回的時候這個棧就會一直pop到入口地址位置,回到調(diào)它的那個函數(shù)的地址,這樣你就知道函數(shù)棧調(diào)用是怎么回事了。

一個棧最大的空間為多少呢?可以執(zhí)行ulimit -s或者ulimit -a命令,它會打印出當前操作系統(tǒng)的內(nèi)存棧最大值:

ulimit -a
stack size (kbytes, -s) 8192

這里為8Mb,相對于一些OS默認的64Kb,已經(jīng)是一個比較大的值了。一旦超出這個值,就會發(fā)生棧溢出stack overflow.

理解了第一條指令和第二條指令的意思后就不難理解第三條和第四條了。第三條是把內(nèi)存地址為[rbp - 8]放到ecx寄存器里面,第四條做一個加法,把[rbp - 12]加到ecx寄存器。就樣就完成了c = a + b的加法。

更多匯編和機器碼的運算讀者有興趣可以自行去查資料繼續(xù)擴展,這里我提了一下,幫助讀者理解這種比較較陌生的機器碼是怎么回事,也是為了下面講解WASM.

2. 編譯和解釋

我們知道編程語言分為兩種,一種是編譯型的如C/C++,另一種是解釋型如Java/Python/JS等。

在編譯型語言里面,代碼需經(jīng)過以下步驟轉成機器碼:

先把代碼文本進行詞法分析、語法分析、語義分析,轉成匯編語言,其實解釋型語言也是需要經(jīng)過這些步驟。通過詞法分析識別單詞,例如知道了var是一個關鍵詞,people這個單詞是自定義的變量名字;語法分析把單詞組成了短句,例如知道了定義了一個變量,寫了一個賦值表達式,還有一個for循環(huán);而語義分析是看邏輯合不合法,例如如果賦值給了this常量將會報錯。

再把匯編再翻譯成機器碼,匯編和機器碼是兩個比較接近的語言,只是匯編不需要去記住哪個數(shù)字代表哪個指令。

編譯型語言需要在運行之前生成機器碼,所以它的執(zhí)行速度比較快,比解釋型的要快若干倍,缺點是由于它生成的機器碼是依賴于那個平臺的,所以可執(zhí)行的二進制文件無法在另一個平臺運行,需要再重新編譯。

相反,解釋型為了達到一次書寫,處處運行(write once, run evrywhere)的目的,它不能先編譯好,只能在運行的時候,根據(jù)不同的平臺再一行行解釋成機器碼,導致運行速度要明顯低于編譯型語言。

如果你看Chrome源碼的話,你會發(fā)現(xiàn)V8的解釋器是一個很復雜的工程,有200多個文件:

最后終于可以來講WebAssembly了。

3. WebAssembly介紹

WASM的意義在于它不需要JS解釋器,可直接轉成匯編代碼(assembly code),所以運行速度明顯提升,速度比較如下:

通過一些實驗的數(shù)據(jù),JS大概比C++慢了7倍,ASM.js官網(wǎng)認為它們的代碼運行效率是用clang編譯的代碼的1/2,所以就得到了上面比較粗糙的對比。
Mozilla公司最開始開發(fā)asm.js,后來受到Chrome等瀏覽器公司的支持,慢慢發(fā)展成WASM,W3C還有一個專門的社區(qū),叫WebAssembly Community Group。
WASM是JS的一個子集,它必須是強類型的,并且只支持整數(shù)、浮點數(shù)、函數(shù)調(diào)用、數(shù)組、算術計算,如下使用asm規(guī)范寫的代碼做兩數(shù)的加法:

function () {
    "use asm";
    function add(x, y) {
        x = x | 0;
        y = y | 0;
        return x | 0 + y | 0;
    }
    return {add: add};
}

正如asm.js官網(wǎng)提到的:

An extremely restricted subset of JavaScript that provides only strictly-typed integers, floats, arithmetic, function calls, and heap accesses

WASM的兼容性,如caniuse所示:

最新的主流瀏覽器基本上已經(jīng)支持。

4. WASM Demo

(1)準備

Mac電腦需要安裝以下工具:

cmake make Clang/XCode
Windows需要安裝:

cmake make VS2015 以上

然后再裝一個

WebAssembly binaryen (asm2Wasm)

(2)開始

寫一個add.asm.js,按照asm規(guī)范,如下圖所示:

然后再運行剛剛裝的工具asm2Wasm,就可以得到生成的wasm格式的文本,如下圖所示

可以看到WASM比較接近匯編格式,可以比較方便地轉成匯編。

如果不是在控制臺輸出,而是輸出到一個文件,那么它是二進制的。運行以下命令:

../bin/asm2wasm add.asm.js -o add.wasm

打開生成的add.wasm,可以看到它是一個二進制的:

有了這個文件之后怎么在瀏覽器上面使用呢,如下代碼所示,使用Promise,與WebAssembly相關的對象本身就是Promise對象:

fetch("add.wasm").then(response =>
    response.arrayBuffer())
.then(buffer => 
    WebAssembly.compile(buffer))
.then(module => {
    var imports = {env: {}};
    Object.assign(imports.env, {
        memoryBase: 0,
        tableBase: 0,
        memory: new WebAssembly.Memory({ initial: 256, maximum: 256 }), 
        table: new WebAssembly.Table({ initial: 0, maximum: 0, element: 'anyfunc' })
   })
   var instance =  new WebAssembly.Instance(module, imports)
   var add = instance.exports.add;
   console.log(add, add(5, 6));
})

先去加載add.wasm文件,接著把它編譯成機器碼,再new一個實例,然后就可以用exports的add函數(shù)了,如下控制臺的輸出:

可以看到add函數(shù)已經(jīng)變成機器碼了。

現(xiàn)在來寫一個比較有用的函數(shù),斐波那契函數(shù),先寫一個asm.js格式的,如下所示:

function fibonacci(fn, fn1, fn2, i, num) {
    num = num | 0;
    fn2 = fn2 | 0;
    fn = fn | 0;
    fn1 = fn1 | 0;
    i = i | 0;
    if(num < 0)  return 0;
    else if(num == 1) return 1;
    else if(num == 2) return 1;
    while(i <= num){
        fn = fn1;
        fn1 = fn2;
        fn2 = fn + fn1;
        i = i + 1;
    }   
    return fn2 | 0;
}

這里筆者最到一個問題,就是定義的局部變量無法使用,它的值始終是0,所以先用傳參的方式。

然后再把剛剛那個加載編譯的函數(shù)封裝成一個函數(shù),如下所示:

loadWebAssembly("fibonacci.wasm").then(instance => {
    var fibonacci = instance.exports.fibonacci;
    var i = 4, fn = 1, fn1 = 1, fn2 = 2;
    console.log(i, fn, fn1, fn2, "f(5) = " + fibonacci(5));
});

最后觀察控制臺的輸出:

可以看到在f(47)的時候發(fā)生了溢出,在《JS與多線程》這一篇提到JS溢出了會自動轉成浮點數(shù),但是WASM就不會了,所以可以看到WASM/ASM其實和JS沒有直接的關系,只是說你可以用JS寫WASM,雖然官網(wǎng)的說法是ASM是JS的一個子集,但其實兩者沒有血肉關系,用JS寫ASM你會發(fā)現(xiàn)非常地笨拙和不靈活,編譯成WASM會有各種報錯,提示信息非常簡陋,總之很難寫。但是不用沮喪,因為下面我們會提到還可以用C寫。

然后我們可以做一個兼容,如果支持WASM就去加載wasm格式的,否則加載JS格式,如下所示:

5. JS和WASM的速度比較

(1)運行速度的比較

如下代碼所示,計算1到46的斐波那契值,然后重復一百萬次,分別比較wasm和JS的時間:

//wasm運行時間
loadWebAssembly("fib.wasm").then(instance => {
    var fibonacci = instance.exports._fibonacci;
    var num = 46;
    var count = 1000000;
    console.time("wasm fibonacci");
    for(var k = 0; k < count; k++){
        for(var j = 0; j < num; j++){
            var i = 4, fn = 1, fn1 = 1, fn2 = 2;
            fibonacci(fn, fn1, fn2, i, j);
        }
    }
    console.timeEnd("wasm fibonacci");
});

//js運行時間
loadWebAssembly("fibonacci.js", {}, "js").then(instance => {
    var fibonacci = instance.exports.fibonacci;
    var num = 46;
    var count = 1000000;
    console.time("js fibonacci");
    for(var k = 0; k < count; k++){
        for(var j = 0; j < num; j++){
            var i = 4, fn = 1, fn1 = 1, fn2 = 2;
            fibonacci(fn, fn1, fn2, i, j);
        }
    }
    console.timeEnd("js fibonacci");
});

運行四次,比較如下:

可以看到,在這個例子里面WASM要比JS快了一倍。

然后再比較解析的時間

(2)解析時間比較

如下代碼所示:

console.time("wasm big content parse");
loadWebAssembly("big.wasm").then(instance => {
    var fibonacci = instance.exports._fibonacci;
    console.timeEnd("wasm big content parse");
    console.time("js big content parse");
    loadJs();
});

function loadJs(){
   loadWebAssembly("big.js", {}, "js").then(instance => {
       var fibonacci = instance.exports.fibonacci;
       console.timeEnd("js big content parse");
   });
}

分別比較解析100、2000、20000行代碼的時間,統(tǒng)計結果如下:

WASM的編譯時間要高于JS,因為JS定義的函數(shù)只有被執(zhí)行的時候才去解析,而WASM需要一口氣把它們都解析了。

上面表格的時間是一個什么概念呢,可以比較一下常用庫的解析時間,如下圖所示:

(3)文件大小比較

20000行代碼,wasm格式只有3.4k,而壓縮后的js還有165K,如下圖所示:

所以wasm文件小,它的加載時間就會少,可以一定程度上彌補解析上的時間缺陷,另外可以做一些懶惰解析的策略。

6. WASM的優(yōu)缺點

WASM適合于那種對計算性能特別高的,如圖形計算方面的,缺點是它的類型檢驗比較嚴格,寫JS編譯經(jīng)常會報錯,不方便debug。

WASM官網(wǎng)提供的一個WebGL + WebAssembly坦克游戲如下所示:

它的數(shù)據(jù)和函數(shù)都是用的wasm格式:

7. C/Rust寫前端

WASM還支持用C/Rust寫,需要安裝一個emsdk。然后用C函數(shù)寫一個fibonacci.c文件如下所示:

/* 不考慮溢出 */
int fibonacci(int num){
    if(num <= 0) return 0;
    if(num == 1 || num == 2) return 1;
    int fn = 1,
        fn1 = 1,
        fn2 = fn + fn1;
    for(int i = 4; i <= num; i++){
        fn = fn1;
        fn1 = fn2;
        fn2 = fn1 + fn;
    }
    return fn2;
}

運行以下命令編譯成一個wasm文件:

emcc fibonacci.c -Os -s WASM=1 -s SIDE_MODULE=1 -o fibonacci.wasm

這個wasm和上面的是一樣的格式,然后再用同樣的方式在瀏覽器加載使用。

用C寫比用JS寫更加地流暢,定義一個變量不用在后面寫一個“| 0”,編譯起來也非常順暢,一次就過了,如果出錯了,提示非常友好。這就可以把一些C庫直接挪過來前端用。

8. WASM對寫JS的提示

WASM為什么非得強類型的呢?因為它要轉成匯編,匯編里面就得是強類型,這個對于JS解釋器也是一樣的,如果一個變量一下子是數(shù)字,一下子又變成字符串,那么解釋器就得額外的工作,例如把原本的變量銷毀再創(chuàng)建一個新的變量,同時代碼可讀性也會變差。所以提倡:
定義變量的時候告訴解釋器變量的類型
不要隨意改變變量的類型
函數(shù)返回值類型是要確定的

這個我在《Effective前端8:JS書寫優(yōu)化》已經(jīng)提到.
到此,介紹完畢,通過本文應該對程序的編譯有一個直觀的了解,特別是代碼是怎么變成機器碼的,還有WebAssembly和JS的關系又是怎么樣的,Webassembly是如何提高運行速度,為什么要提倡強類型風格代碼書寫。對這些問題應該可以有一個理解。
另外一方面,web前端技術的發(fā)展真的是非常地活躍,在學這些新技術的同時,別忘了打好基本功。

原文:極樂科技知乎專欄

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,797評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,179評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,628評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,642評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,444評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,948評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,040評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,185評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,717評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,602評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,794評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,316評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,045評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,418評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,671評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,414評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,750評論 2 370

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