淺談WebAssmebly,在浏覽器上進行圖像處理

自從2008年各大浏覽器為JavaScript執行引擎加入JIT(Just in Time, 即時編譯器)以來,JavaScript在浏覽器的執行性能有了一個量級的飛躍提升,促進了Web領域的飛速發展,一大波大型Web應用豐富了我們的網絡世界。但是,JIT帶來的性能提升也有天花闆,在處理複雜運算的情況下表現仍有不足,為了更進一步解決JavaScript的性能問題,WebAssembly就應運而生了。
(本文非例子部分的圖檔出自文章1. https://hacks.mozilla.org/2017/02/a-cartoon-intro-to-webassembly/,文字部分基本參考該文章)
1. WebAssembly簡介
1.1 什麼是WebAssembly?
1.2 誰做的?
1.3 JavaScript性能曆史
2. WebAssembly如何工作
3. 為什麼WebAssembly更快?
3.1 JIT原理
3.2 JIT與WebAssembly的時間耗時對比
4. WebAssembly初體驗
4.1 調用C函數
4.2 在浏覽器上進行圖像處理
5. 總結
Refrence
WebAssembly簡介
什麼是WebAssembly?
首先我們先簡單了解一下什麼是WebAssembly。
- 它是一種新的、跨平台的web編譯技術,可以讓衆多靜态語言編譯出浏覽器可以運作的代碼檔案,檔案格式為wasm,是一種相當接近機器語言的二進制格式檔案;
- 相比于文本格式的JavaScript檔案來說有着體積小、下載下傳速度快的特點;
- 因為是從強類型語言編譯的,是以JavaScript引擎不用猜測變量類型,也不用經曆棄優化、重優化步驟,是以代碼運作得更快;
- 這種技術出現,并不是為了替代JavaScript,而是作為JavaScript的一個良好補充;
誰做的?
WebAssembly是我們熟知的四大主流浏覽器廠商 Google Chrome、Apple Safari、Microsoft Edge 和 Mozilla FireFox 共同推動的,他們覺得Mozilla FireFox所推出的asm.js很有前景,為了讓大家都能使用,于是他們就共同參與開發,基于asm.js制定一個标準,也就是WebAssembly。
2017 年 3 月份, 四大廠商均宣布已經于最新版本的浏覽器中支援了 WebAssembly 的初始版本,這意味着 WebAssembly 技術已經實際落地,可以在特定生産環境進行嘗試。
JavaScript性能曆史
WebAssembly的出現并不是偶然,讓我們回顧一下JavaScript的性能發展史。
1995年,大神布蘭登·艾奇僅僅花了10天就将我們偉大的JavaScript撸出來了。
(蜜汁微笑)
當初語言的設計初衷是想設計出一個面向非專業程式設計人員和網頁設計師的解釋型語言。由于設計時間太短,語言的一些細節考慮得不夠嚴謹,導緻留下了不少坑,後來的很長一段時間,JavaScript的執行速度一直備受诟病。直到2008年,浏覽器的性能大戰打響,衆多浏覽器引入了即時編譯(也就是我們熟知的JIT編譯器),使得JavaScript運作速度快了一個量級。(大拐點)
也是由于JIT的出現,使得JavaScript的應用範圍從浏覽器走到了伺服器領域、桌面應用、移動應用等領域,大放異彩。
但是我們對性能的期望是無盡的,JIT帶來的性能提升也早已因為JavaScript的動态特性達到了性能天花闆,在複雜運算上仍然力不從心,特别在目前Web遊戲的日益增多的情況下,問題顯得日益突出。
那麼我們很可能會有這樣的疑問:下一個性能提升拐點是什麼呢?
很有可能就是WebAssembly。
WebAssembly如何工作
工作原理:WebAssembly的工作原理簡要來說是将C,C++, Rust等靜态語言的程式編譯成浏覽器能夠運作的wasm二進制檔案,當浏覽器加載wasm檔案後編譯為本地機器碼後運作。
我們知道大多數靜态進階語言是通過編譯器前端編譯成為中間代碼,然後再由編譯器後端把中間代碼翻譯成目标機器的可執行機器碼的。
而wasm對應的位置則是生成特定平台機器碼之前,類似于一種彙編語言,但是它不對應真實的實體機器,而是一種對應抽象的機器,比JavaScript源碼更直接地對應機器碼。浏覽器下載下傳wasm檔案後隻需要做簡單的編譯就能執行。
那麼如何編譯出wasm檔案呢,目前支援度最好的編譯工具鍊是LLVM,Emscripten就是目前最常用一個使用LLVM、開源的SDK,前端采用clang,用clang将C代碼變為LLVM中間代碼,之後LLVM會對這些代碼進行優化,而後端就是一個名為Fastcomp的LLVM 後端,它既可以直接wasm,也可以先生成asm.js,再轉為wasm。
對于以下的一個C函數,通過Emscripten sdk就能生成二進制格式的wasm檔案了。
int add42 (int num) {
return num + ;
}
用編輯器打開wasm檔案就可以看到熟悉的一堆十六進制數:
D D
F F
D
D F F A
A D
A A B
這裡需要簡單提一下,wasm檔案還有一種等價的表達方式,就是下圖中最下邊的文本格式,是用S-表達式來表示的,它是一種源代碼到WebAssembly的中間狀态,讓我們能夠更好地讀懂wasm檔案,調試、或者徒手撸出WebAssembly,因為S-表達式與WebAssembly一一對應,是以可以很好地互相轉化。在浏覽器加載wasm檔案并編譯完成後會生成相應文本格式,在控制台中可以檢視WebAssembly的文本格式檔案wast。
為什麼WebAssembly更快?
一說到WebAssembly,許多文章都會提及它的快,它的性能優勢都是相對于JavaScript來說的,但是他為什麼快呢?
JIT原理
為了了解為什麼WebAssembly有更好的性能,我們首先需要簡單過一下浏覽器JIT的工作原理。
首先JavaScript引擎的工作就是把我們看得懂的程式設計語言轉換成機器能看懂的語言,我們與機器的溝通媒體就是JavaScript引擎,沒有這個翻譯官,我們下達的指令機器就沒法了解和執行。
程式設計語言的翻譯有兩種方法:
- 使用解釋器
- 使用編譯器
解釋器是一部分一部分地邊解釋邊執行。
編譯器是提前把源代碼整個編譯成目标代碼,直接在支援目标代碼的平台上運作,執行過程不需要編譯器。
兩種方法各有利弊:
解釋器好處除了易于實作跨平台外,最直接的就是對于我們前端開發人員來說,調試頁面時,修改一行代碼可以立即看到結果,不需要等待編譯過程。但是對于同樣的代碼需要執行多次的情況弊端就很明顯了,它需要執行多次的解釋,就比如說執行循環。
編譯器則可以在編譯的時候可以對這些重複的代碼等進行優化,使得執行得更快。由于花了許多時間在提前優化上,是以相對地需要犧牲的就是編譯代碼的時間。
在JIT出現以前,JavaScript大多都是從由JavaScript引擎解釋執行的,是以效率低下,為了解決性能問題,其中一個辦法就是引入編譯器,将部分代碼優化編譯,充分結合編譯器的優勢,結合解釋器與即時編譯器以提升性能早已在python等語言上得到很好的實踐證明,是以浏覽器廠商們就紛紛引入了JIT。
JIT的實作方法就是在JavaScript引擎中實作一個螢幕,這個螢幕監視運作的代碼,記錄下代碼各自的運作次數并标記它們的熱點類型。如果發現某一段代碼執行了較多次,将标記為’warm’,如果執行了許多次,那麼就會被标記為’hot’
JIT會把标記為warm的代碼送到基線編譯器(Baseline compiler)中編譯,并且存儲編譯結果,當解釋器繼續解釋時,螢幕發現了同樣的代碼,那麼就會把剛才編譯好的結果推給浏覽器,讓浏覽器使用較快的編譯版本。
在基線編譯器中,代碼會進行一定程度的優化,但是由于優化需要時間,代碼隻是warm狀态,是以基線編譯器并不會花太長時間去優化。
不過如果标記為warm的代碼執行了更多次呢? 代碼已經非常的hot了,這時花更多時間去優化它就非常有必要了,是以螢幕會将這段代碼放到優化編譯器(Optimizing compiler)中,生成更加快速高效的代碼,這時如果螢幕發現了同樣的代碼,就會傳回這個更加快的優化編譯版本。
在優化編譯器中,整個函數被編譯在一起,并按照一個特定形式來優化。不過優化編譯器需要一些前提,就比如它會假設由同一個構造函數生成的執行個體都有相同的屬性名,并且都以同樣的順序初始化,那麼就可以針對這一模式進行優化。也就是說,螢幕根據代碼執行情況做出假設,如果某個循環中先前每次疊代的對象都有相同的形狀,那麼它就會假設以後疊代的對象的形狀都是相同的。螢幕把這些資訊提供給優化編譯器作為前提,進行優化。
然而,由于Javascript是弱類型的語言,它的靈活性可能會導緻在幾百個循環後某一次循環中這個對象少了某個屬性,那麼JIT檢測到後會認為這個優化的編譯代碼不合理,會将這個編譯代碼丢棄掉,轉而使用基線編譯版本或者也可能直接回到解釋器。
這個過程叫做去優化(JIT監視到如果某段代碼進行了幾次優化到去優化的循環後,會終止這段代碼的優化編譯,防止無限的循環,盡管如此,這裡的性能損耗仍不可忽視)
舉個簡單的例子,對于以下這段循環代碼,在基線編譯器階段,每一行代碼會被基線編譯器編譯成代碼樁
function arraySum (arr) {
let sum =
for (let i = ; i < arr.length; i++) {
sum += arr[i]
}
}
但是對于累加
sum += arr[i]
這一句代碼,我們并沒有确定sum和arr是什麼類型,如果是數字,基線編譯器會生成一個樁(stub),如果是字元串,它也會為它生成一個樁,也就是說在調用過程中,可能有一個以上的樁,這時浏覽器再次執行這段代碼時,需要進行多次分支選擇,進行類型檢查。
是以,如果類型不固定,使用的是基線編譯版本,每次循環都要進行一次類型檢查。
如果類型固定,使用的是優化編譯器後的版本,在循環之前就進行一次類型檢查就可以了,速度提升是相當顯著的。
是以,使用JavaScript在靈活與興能上存在一定的折中關系,享受靈活的同時必然有一定的性能損耗。
總的來說通過JIT編譯器,JavaScript的性能有了很大的提升,通過使用基線編譯版本或者優化編譯版本,能夠大大減少解釋器的時間損耗。但是JIT也有一定的瓶頸,主要展現在:
- JIT 編譯器花了很多時間在猜測 Javascript 中的類型。
- 在優化和去優化過程中造成了很大開銷
而使用WebAssembly的出現的原因之一,就是為了消除這些開銷。
JIT與WebAssembly的時間耗時對比
我們來看看運作一段JavaScript代碼時,JavaScript引擎所花的時間的大概分布
- 一個是對JavaScript源碼進行解釋,生成抽象文法樹或者位元組碼(parse)
- 一個是JIT編譯器對那些熱點代碼的編譯優化所花的時間(compile + optimize)
- 一個是當發生去優化時,重新優化所花的時間(re-optimize)
- 一個是執行代碼的時間(execute)
- 再一個是就是GC垃圾回收所化的時間(garbage collection)
需要注意的是,這幾個部分的工作線上程中是交替進行的,一段代碼中某一部分可能在解釋、然後某一部分可能在去優化、然後某一部分可能在執行。這裡的圖的順序隻是為了友善描述。
而需要提及的是,過去沒有JIT時,JavaScript的執行時間需要更多的時間,就如同下圖所示:
那麼對于執行一段相同功能的WebAssembly代碼的時間分布大概是怎樣的呢?讓我們逐漸比對一下。
1. 首先是解釋源代碼的時間。
JavaScript源代碼需要由解釋器生成抽象文法樹後再轉為中間代碼(位元組碼),但是WebAssembly不一樣,它本來就是一種中間代碼,隻不過比位元組碼更接近機器碼,是以JavaScript引擎在這裡不需要複雜地解析,隻是解碼并確定wasm沒有錯誤即可。
2. 然後是編譯和優化的時間。
之前說過JIT會在JavaScript執行時進行熱點代碼的編譯優化,相同代碼對于不同變量類型會有不同的編譯版本,于此相對的,WebAssembly因為是靜态語言編譯來的,編譯優化前就已知變量類型,不需要編譯多個版本,并且由于LLVM已經幫忙做了許多優化,是以在這個階段JavaScript引擎消耗的時間是很少的
3. 然後就是重新優化的時間。
在該階段,在類型變化時JIT會有兩大時間開銷,第一就是從優化編譯版本回退到基線編譯版本的時間開銷,第二則是再次檢測到這段代碼被多次調用,再次送到優化編譯器進行優化而造成的反複去優化和重優化所造成的時間開銷,而WebAssembly由于靜态類型則完全不會造成這些開銷。
4. 然後在執行階段。
在執行階段中,如果了解JIT的優化機制的話,我們開發人員能夠根據其寫出執行效率更高的代碼,但是這樣一來代碼的可讀性将會收到很大的影響,因為适合JIT編譯器優化的代碼,對于常見的實作代碼來說就如同hack一樣,并且不同浏覽器的JIT一般隻對自己的浏覽器做優化,在這個浏覽器效率高,在另一個就不一定。但是WebAssembly就不一樣,它是浏覽器無關的,我們不需要懂得太多的編譯器技巧,它就幫我們把所有優化做好了,并且提供了更理想的指令給機器,是以執行階段的效率也是比原生JavaScript要高的。
5. 最後,對于垃圾回收階段。
為了追求性能,目前WebAssembly并不提供GC,除了基本類型都需要手動控制記憶體,以開發成本換取運作性能。不過,目前官方正在進行GC的讨論
是以,最後WebAssembly就隻有解碼、編譯優化和執行這三部分開銷,對比原生性能開銷減少了許多。對比如圖所示
當然,還有一個需要提及的是,wasm是二進制檔案,比高度壓縮過的JavaScript代碼更小,在網絡傳輸上是更快的。
是以這就是WebAssembly在大多數情況下比JavaScript更快的原因。
WebAssembly初體驗
我們接下來用Emscripten SDK 和 c/c++簡單展示一些小例子。首先需要去Emscripten官網參照教程下載下傳安裝并配置開發環境。配置好開發環境後,我們就可以在指令行中運作emcc指令編譯我們的c/c++代碼了。
調用C函數
我們首先展示C語言編寫的斐波那契數列(遞歸)是如何編譯生成wasm檔案,并且在浏覽器中是如何調用的。在某一目路中,C檔案fib.c如下:
#include <stdio.h>
int fib(int n) {
return n <=
?
: fib(n - ) + fib(n - );
}
執行emcc指令:
-o
表示輸出的檔案名,
-s
後緊跟編譯的配置(setting),所有配置項可以在這裡檢視。
EXPORTED_FUNCTIONS
表示我們要導出的方法。
以上指令将會生成一個fib.js檔案,和一個fib.wasm檔案,其中fib.js是膠接代碼,是由于目前WebAssembly還未完成,提供的解析WebAssembly子產品的API還未成熟,使用起來較為複雜,是以Emscripten在編譯輸出wasm的同時為我們提供了膠接代碼。這也是為什麼指令行輸出檔案名為fib.js而不是fib.wasm的原因。
Emscripten提供該膠接代碼有一個模闆,稱為preamble.js (doc、 github),對于不同編譯輸出,都會根據該模闆生成不同的膠接代碼。
生成的膠接代碼在浏覽器中将會使用 fetch api 或者 ajax 的方式擷取wasm檔案,并構造一個子產品,該子產品不僅提供了wasm中導出的方法,也提供了一些有用的JavaScript與WebAssembly的類型的互相轉化方法(例如
_malloc
、
_free
、
stringToC
、
arrayToC
等,這些函數可以将JavaScript的ArrayBuffer轉為WebAssembly代碼函數能夠接受的指針等),建立起JavaScript與WebAssembly互相調用的橋梁,并在window上挂載該子產品。
以下是目前較為常用地擷取WebAssembly檔案的方法,膠接代碼也是使用這一個方法:
function loadWebAssembly (filename, imports = {}) {
return fetch(filename)
.then(response => response.arrayBuffer())
.then(buffer => {
imports.env = imports.env || {}
Object.assign(imports.env, {
memoryBase: ,
tableBase: ,
memory: new window.WebAssembly.Memory({ initial: , maximum: }),
table: new window.WebAssembly.Table({ initial: , maximum: , element: 'anyfunc' })
})
return window.WebAssembly.instantiate(buffer, imports)
})
.then(results => {
return results.instance
})
}
該方法通過fetch拉取二進制檔案,獲得相應的ArrayBuffer,并在本地浏覽器進行編譯解碼,執行個體化子產品,即可調用執行個體化後的WebAssembly子產品上導出的字段、方法、類等。其中
WebAssembly.instantiate
是執行個體化方法,
WebAssembly.Memory
用來方法生成一塊可變的ArrayBuffer記憶體,用于JavaScript與WebAssembly共同通路,主要用來傳遞指針等,
WebAssembly. Table
則用來生成一個存儲函數引用的類數組結構。
在html中通過script标簽将膠接代碼引入:
當其運作完畢後,我們将可以通過window.Module(預設挂載字段名為Module)對象通路各種字段、方法:
我們WebAssembly子產品的方法
fib
也可以擷取:
隻不過方法是以
_
開頭的,這是Emscripten的要求的,通過
EXPORTED_FUNCTIONS
導出的方法需要以下劃線開頭。于是我們可以如下調用fib函數:
當然,如果我們寫的c檔案中如果要導出許多函數呢?這樣在指令行中寫一長串函數清單在
EXPORTED_FUNCTIONS
中是不是相當的麻煩呢?是以Emscripten給我們提供了一個很有用的宏定義
EMSCRIPTEN_KEEPALIVE
,該定義将使得LLVM不删除該函數,等同于導出該函數(
EXPORTED_FUNCTIONS
),是以fib.c可以寫成這樣:
#include <stdio.h>
#include <emscripten.h>
int EMSCRIPTEN_KEEPALIVE fib(int n) {
return n <=
?
: fib(n - ) + fib(n - );
}
指令行則可以省略
EXPORTED_FUNCTIONS
:
emcc fib.c -o fib.js -s WASM=
通過上述步驟,我們就可以在浏覽器中調用c語言提供的函數了,一個簡單的小例子就此完成~
在浏覽器上進行圖像處理
上述例子隻是簡單展示了在浏覽器上如何調用WebAssembly函數,但是我們并沒有展示出WebAssembly是怎樣快速而又強大的。是以,接下來我們将展示在浏覽器上進行圖像處理,并比較JavaScript與WebAssembly的處理速度。
首先,說到圖像處理,我們不得不提到著名的計算機視覺庫OpenCV,它輕量級而且高效,由C++編寫而成,并提供了Python、MATLAB、Java等語言的接口,實作了圖像進行中的許多常用的通用算法和先進的算法。在這裡,我們将通過Emscripten将OpenCV的部分接口和處理函數編譯為WebAssembly檔案,并在浏覽器中進行調用,同時,我們也将實作JavaScript版本的處理方法,與WebAssembly進行對比。
在上一個例子中,我們實作的是JavaScript調用C函數,然而它并不适用于name mangled的C++函數,Emscripten提供了兩個工具來實作C++和JavaScript的綁定,WebIDL Binder和Embind。兩個工具讓我們能夠讓C++的代碼能夠像JavaScript那樣進行調用,WebIDL Binder支援能夠被WebIDL表示的C++類型,而Embind則支援幾乎所有的C++代碼。WebIDL Binder的支援度較Embind是少許多的,不過在大多數情況下,它是足夠适用的,例如一些有趣的實體引擎項目Box2D和Bullet(ammo.js),就是使用了WebIDL Binder。Embind則是受Boost.Python所啟發,提供了更好的JavaScript到C++的橋梁,除了能夠在JavaScript中任意調用C++提供的字段、函數、類等,還能反過來在C++中調用JavaScript,在這個例子中,我們使用Embind來完成綁定。
在編寫綁定的cpp檔案(bindings.cpp)時,我們需要添加如下代碼:
#include <emscripten/bind.h>
using namespace emscripten;
包含Embind工具的頭檔案,并聲明使用emscripten命名空間。這時,我們就可以在代碼中使用各種綁定函數了。綁定需要使用
EMSCRIPTEN_BINDINGS()
代碼塊。
例如C++類的綁定,我們可以:
#include <emscripten/bind.h>
using namespace emscripten;
class CppClass {
public:
CppClass(int x, std::string y)
: x(x)
, y(y)
{}
void incrementX() {
++x;
}
int getX() const { return x; }
void setX(int x_) { x = x_; }
static std::string getStringFromInstance(const CppClass& instance) {
return instance.y;
}
private:
int x;
std::string y;
};
EMSCRIPTEN_BINDINGS(my_module) {
class_<CppClass>("JsUseClass") // 綁定C++類CppClass,命名為"JsUseClass"
.constructor<int, std::string>() // 構造函數接收參數類型為[int, string]
.function("incrementX", &CppClass::incrementX) // 綁定C++類CppClass的成員函數incrementX,命名為"incrementX"
.property("x", & CppClass::getX, &CppClass::setX) // 綁定C++類CppClass的屬性x,命名為"x"
.class_function("getStringFromInstance", &CppClass::getStringFromInstance); // 綁定C++類CppClass的靜态函數getStringFromInstance,命名為"getStringFromInstance"
}
對于一些結構體資料,Emscripten提供了
value_array
和
value_object
來讓我們将它們轉化為JavaScript的數組和對象:
#include <emscripten/bind.h>
using namespace emscripten;
struct Point2f {
float x;
float y;
};
struct PersonRecord {
std::string name;
int age;
};
PersonRecord findPersonAtLocation(Point2f);
EMSCRIPTEN_BINDINGS(my_value_example) {
value_array<Point2f>("Point2f")
.element(&Point2f::x)
.element(&Point2f::y)
;
value_object<PersonRecord>("PersonRecord")
.field("name", &PersonRecord::name)
.field("age", &PersonRecord::age)
;
function("findPersonAtLocation", &findPersonAtLocation);
}
對于常量和枚舉,則可以:
#include <emscripten/bind.h>
using namespace emscripten;
enum class NewStyle {
ONE,
TWO
};
const int SOME_CONSTANT = ;
EMSCRIPTEN_BINDINGS(my_enum_example) {
enum_<NewStyle>("NewStyle")
.value("ONE", NewStyle::ONE)
.value("TWO", NewStyle::TWO)
;
constant("SOME_CONSTANT", SOME_CONSTANT);
}
Emscripten Embind
還提供了
typed_memory_view
和
val
兩個方法,
typed_memory_view
用于在JavaScript中能夠将C++中的記憶體指針作為TypeArray來通路,而
val
則用于在C++中能夠直接通路JavaScript中的對象,進行各種操作。
更多的用法可以去官方文檔、官方API和頭檔案定義查閱。
編寫好綁定檔案(bindings.cpp)後,我們執行以下指令進行編譯即可:
emcc --bind -o bindings.js bindings.cpp -s WASM=
--bind
表示使用
Embind
來建立C++與JavaScript的綁定。輸出同樣為bindings.js和bindings.wasm兩個檔案。
對于C++類的綁定的例子來說,我們綁定了C++類CppClass并命名為”JsUseClass”,讓JavaScript可以通過JsUseClass來調用。在html中使用
script
标簽引入bindings.js腳本之後我們可以在控制台中可以看到:
可以看到,我們能夠使用C++所實作的類CppClass的任何屬性和方法。
是以,通過
Emscripten
和它的
Embind
工具,我們能夠将C++項目編譯成WebAssembly,在浏覽器中調用。
當然OpenCV也不例外,将OpenCv編譯成WebAssembly後,我們就可以在浏覽器中進行複雜地圖像處理操作了。誠然,将整個OpenCV編譯成WebAssembly是一件不容易的事情,不僅需要将每一個接口進行綁定,還需要花大量時間在配置編譯參數上。是以,我們可以僅導出OpenCV通用的一些類型如
Mat
、
Size
、
Point
等,和我們所需要的處理函數,如
Canny
、
dft
等,而在配置編譯參數上,我們可以利用Emscripten提供的一些python工具類如shared.py等,來幫助我們構造編譯腳本。
對于OpenCV編譯到WebAssembly,github上已經有一個項目opencvjs實作了,作者正是利用Embind工具和上述的shared.py來完成編譯的,由于篇幅關系,本文中不一一說明其編譯的步驟,綁定程式和編譯腳本可以給我們作為一個很好的例子。
為了對比JavaScript和WebAssembly的性能,我們在浏覽器中進行幾個算法的對比。
注意:
- 實作的算法不完全一緻,JavaScript中可能無法使用OpenCV中的優化方法;
- 耗時中,包括了由ImageData轉為OpenCV的Mat或JavaScript數組以供圖像處理的時間,OpenCV WebAssembly版本包括了用JavaScript處理由Mat轉為ImageData以供canvas顯示的時間,而JavaScript版本并不包括,展現在一些簡單操作上JavaScript版本稍快;
- JavaScript方法和WebAssembly方法都跑在WebWorker中
機器參數:
Mac mini (Late 2014)
處理器 2.6 GHz Intel Core i5
記憶體 16 GB 1600 MHz DDR3
顯示卡 Intel Iris 1536 MB
浏覽器:
Google Chrome 版本 61.0.3163.100(正式版本) (64 位)
圖像大小:
256 * 256
圖檔灰階化:
将彩色圖處理為灰階圖像,該處理較為簡單,從結果中可以看到OpenCV WebAssembly版本速度稍快,但優勢不明顯。
sobel算子:
一種邊緣提取算法,由于需要進行卷積計算,處理較為複雜,可以看到OpenCV WebAssembly版本明顯快了許多。
圖像頻域低通濾波:
使用快速傅裡葉變換對圖像進行頻域處理,頻域圖像中中心部分是低頻資訊,對應圖像的概貌,頻域圖像離中心遠的部分是高頻資訊,對應圖像的細節,過濾掉一些高頻資訊後逆變換得到的處理結果圖檔,可以看到肉眼是很難區分處理結果與原圖的差別的,而結果與原圖作差後,再進行對數處理,可以看到有一定的細節資訊出現在difference圖中。快速傅裡葉變換是一種較為複雜的算法,可以看到,OpenCV WebAssembly版本在頻域處理階段和逆變換階段中都明顯快了許多。
圖像頻域添加盲水印:
圖像頻域處理有很多作用,可以實作一些空間域上難以實作的處理,包括濾波、去噪、增強等,當然也可以添加頻域盲水印。我們這裡實作了非常簡單低配的水印嵌入,僅僅是在頻域圖像上的高頻部分加入了’666666’等字元,可以看到逆變換後的處理結果圖檔與原圖在肉眼上根本分不清差別,我們的超低配版盲水印成功的嵌入了圖檔中,隻有通過頻域正變換才能夠看到(需要指出的是,這裡的盲水印例子僅僅是為了展示,真實的頻域盲水印并不是這樣處理的,它更為複雜,不僅可能會在頻域進行中引入小波變換,對水印進行加密而不是直接出現在頻域圖中,并且在識别時也可能不允許通過對比原水印資訊來恢複水印,而是通過特征門檻值來從水印庫中識别出來)。同樣的,OpenCV WebAssembly版本妥妥的勝出,完全可以在浏覽器中做到實時處理。
通過幾個例子可以看到,OpenCV WebAssembly版本的性能相當強勁,雖然例子中JavaScript的速度稍慢有一部分原因是實作方法不夠優秀,但是有一點毋庸置疑的是,通過WebAssembly,我們可以直接使用C++大量的優秀項目,而不需要使用JavaScript重新制作輪子,同時享受WebAssembly帶來的性能的提升,在浏覽器中進行更多的複雜運算,做更多有趣的事情,除了我們提到的圖像處理,還包括視訊解碼、WebVR/AR、Web遊戲、加密算法等等。以下再列舉網上幾個WebAssembly的例子:
Unity tutorial game
人臉、人眼檢測
game ZenGarden
asm-dom WebAssembly虛拟DOM
jq-web JSON指令行處理器jq的WebAssembly版本
總結
總的來說,WebAssembly是一種能夠由多種靜态語言編譯而成的一種新的二進制格式代碼,它的目的是能夠被浏覽器fetch更快,在浏覽器執行得更快,以實作更多複雜的應用,并且能夠在浏覽器與JavaScript能夠友好地互相協作,作為JavaScript的一個良好補充。随着WebAssembly的不斷改進和發展,越來越多的靜态語言能夠參與到Web的開發中來,與JavaScript互相協作,浏覽器似乎真的越來越像一個小型的“作業系統”了。
Refrence
- https://hacks.mozilla.org/2017/02/a-cartoon-intro-to-webassembly/
- https://webassembly.github.io/spec/intro/index.html
- https://github.com/kripken/emscripten/wiki
- http://webassembly.org/docs/high-level-goals/
- https://www.zhihu.com/question/31415286
- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly