一個白學家眼裏的 WebAssembly

在知乎「如何看待 WebAssembly 技術」的問題裏,能夠看出你們廣泛對瀏覽器、WASM 和 JS 之間的三角關係有很多誤解。所以這裏做爲一個開 (bai) 發 (xue) 者 (jia),我就來嘗試糾正些常見的問題吧。javascript

全文觀點摘要:WASM 運行時性能在原理上就是受限的,甚至 JS 均可以和編譯到 WASM 的 Rust 一較高下。加上工具鏈的高度侵入性,它並不太適合做爲前端背景同窗 all in 的方向,但對於原生應用的跨平臺分發則很是有潛力html

WASM == 彙編級性能?

這顯然不對,WASM 裏的 Assembly 並不意味着真正的彙編碼,而只是種新約定的字節碼,也是須要解釋器運行的。這種解釋器確定比 JS 解釋器快得多,但天然也達不到真正的原生機器碼水平。一個可供參考的數據指標,是 JS 上了 JIT 後總體性能大體是機器碼 1/20 的水平,而 WASM 則能夠跑到機器碼 1/3 的量級(視場景不一樣很很差說,僅供參考)。至關於即使你寫的是 C++ 和 Rust 級的語言,獲得的其實也只是 Java 和 C# 級的性能。這也能夠解釋爲何 WASM 並不能在全部應用場景都顯示出壓倒性的性能優點:只要你懂得如何讓 JS 引擎走在 Happy Path 上,那麼在瀏覽器裏,JS 就敢和 Rust 五五開前端

一個在 WASM 和 JS 之間作性能對比的經典案例,就是 Mozilla 開發者和 V8 開發者的白學現場。整個過程是這樣的:java

巧的是,這場論戰正發生在兩年前白色相簿的季節。雙方就像雪菜和冬馬那樣展開了高水平的對決,名場面十分精彩。最終 Vyacheslav 給出了一張三輪過招後的性能對比圖。能夠看到雖然最終仍是 Rust 更快,但 JS 被逼到極限後非但不是敗犬,還勝出了一回合:node

JS:先開始性能優化的是你吧!擅自跑到我沒法觸及的地方去的人是你吧!明明高不可攀,卻又近在咫尺,先想出這種拷問方式的人是你吧!明明是這樣,爲何還非得被你責備不可啊…?像那樣…天天、天天,在個人眼前,跑得那麼快…還說這全都是個人錯…太殘忍了啊…linux

另外,Milo Yip 大大作過的不一樣語言光線追蹤性能測試(修羅場),也能側面印證帶 VM 語言與機器碼之間的性能對比結論。C++、Java 和 JS 在未經特別優化的前提下,能夠分別表明三個典型的性能檔次:android

C++/C#/F#/Java/JS/Lua/Python/Ruby 渲染比試git

Ruby:爲何大家都這麼熟練啊!程序員

WASM 比 JS 快,因此計算密集型應用就該用它?

這有點偏頗,WASM 一樣是 CPU 上的計算。對於能夠高度並行化的任務,使用 WebGL 來作 GPU 加速每每更快。譬如我在 實用 WebGL 圖像處理入門 這篇文章裏介紹的圖像處理算法,比起 JS 裏 for 循環遍歷 Canvas 像素就能夠很輕鬆地快個幾十倍。而這種套兩層 for 循環的苦力活,用如今的 WASM 重寫能快幾倍就很是不錯了。至於瀏覽器內 AI 計算的性能方面,社區的評測結論也是 WebGL 和 WebMetal 具有最高的性能水平,而後纔是 WASM。參見這裏:瀏覽器內的 AI 評測github

不過,WebGL 的加速存在精度問題。例如前端圖像縮放庫 Pica,它的核心用的是 Lanczos 採樣算法。我用 WebGL 着色器實現過這個算法,它並不複雜,早期的 Pica 也曾經加入過可選的 WebGL 優化,但如今卻劈腿了 WASM。這一決策的理由在於,WASM 能保證相同參數下的計算結果和 JS 一致,但 WebGL 則不行。相關討論參見這裏:Issue #114 · nodeca/pica

因此對計算密集型任務,WASM 並非前端惟一的救星,而是給你們多了一種在性能、開發成本和效果之間權衡的選擇。在我我的印象裏,前端在圖形渲染外須要算力的場景說實話並不太多,像加密、壓縮、挖礦這種,都難說是高頻剛需。至於將來可能至關重要的 AI 應用,長期而言我仍是看好 WebGPU 這種更能發揮出 GPU 潛力的下一代標準,固然 WASM 也已是個不錯的可選項了。

WebGL:是我,是我先,明明都是我先來的…3D 也好,圖像處理也好,仍是深度學習也好…

只要嵌入 WASM 函數到 JS 就能提升性能?

既然 WASM 很快,那麼是否是我只要把 JS 裏 const add (a, b) => a + b 這樣的代碼換成用 C 編譯出來的 WASM,就能夠有效地提升性能了呢?

這還真不必定,由於現代瀏覽器內的 JS 引擎都標配了一種東西,那就是 JIT。簡單來講,上面這個 add 函數若是始終都在算整數加法,那麼 JS 引擎就會自動編譯出一份計算 int a + int b 的機器碼來替代掉原始的 JS 函數,這樣高頻調用這個函數的性能就會獲得極大的提高,這也就是 JIT 所謂 Just-in-time 編譯的奧妙所在了。

因此,不要一以爲 JS 慢就想着手動靠 WASM 來嵌入 C,其實現代 JS 引擎可都是在不停地幫你「自動把 JS 轉換成 C」的!若是你能夠把一個 JS 函數改寫成等價的 C,那麼我猜若是把這個函數單獨抽離出來,靠 JS 引擎的 JIT 都極可能達到相近的性能。這應該就是 V8 開發者敢用 JS 和 Rust 對線的底氣所在吧。

像在 JS 和 WASM 之間的調用終於變快了 這篇文章中,Lin Clark 很是精彩地論述了整個優化過程,最終使得 JS 和 WASM 間的函數調用,比非內聯的 JS 函數間調用要快。不過,至於和被 JIT 內聯掉的 JS 函數調用相比起來如何,這篇文章就沒有說起了。

這裏偏個題,Mozilla 常常宣傳本身實現的超大幅優化,有很多均可能來源於以前明顯的設計問題(平心而論,咱們本身未嘗不是這樣呢)。像去年 Firefox 70 在 Mac 上實現的 大幅省電優化,其根源是什麼呢?粗略的理解是,之前的 Firefox 在 Mac 上居然每幀都會全量更新窗口像素!固然,這些文章的乾貨都至關多,十分推薦你們打好基礎後看看原文,至少是個更大的世界,也經常能對軟件架構設計有所啓發。

若是後續 WASM 支持了 GC,那麼嵌入互調的狀況極可能更復雜。例如我最近就嘗試在 Flutter 的 Dart 和安卓的 Java 之間手動同步大對象,但願能「嵌入一些安卓平臺能力到 Flutter 體系裏」,然而這帶來了許多冗長而低性能的膠水代碼,須要經過異步的消息來作深拷貝,可控性很低。雖然 WASM 如今尚未 GC,但一旦加上,我有理由懷疑它和 JS 之間的對象生命週期管理也會遇到相似的問題。只是這個問題主要是讓 Mozilla 和 Google 的人來操心,用不着咱們管而已。

參數傳遞什麼的,已經無所謂了。由於已經再也不有函數,值得去調了。 傳達不了的指針,已經不須要了。由於已經再也不有對象,值得去愛了。

在 JS 裏調 WASM,就像 Python 裏調 C 那樣簡單?

這個問題只有實際作過纔有發言權。譬如我最近嘗試過的這些東西:

  • 在安卓的 Java class 裏調用 C++
  • 在 Flutter 的 Dart 裏調用 C
  • 在 QuickJS 這種嵌入式 JS 引擎裏調用 C

它們都能作到一件事,那就是在引擎裏新建原生對象,並將它以傳引用的方式直接交給 C / C++ 函數調用,並用引擎的 GC 來管理對象的生命週期。這種方式通常稱爲 FFI(Foreign Function Interface 外部函數接口),能夠把原生代碼嵌入到語言 Runtime 中。但若是是兩個不一樣的 Runtime,事情就沒有這麼簡單了。例如 QuickJS 到 Java 的 binding 項目 Quack,就須要在 JS 的對象和 Java 對象中作 Marshalling(相似於 JSON 那樣的序列化和反序列化)的過程,不能隨便傳引用。

對 WASM 來講是怎樣的呢?基本上,WASM 的線性內存空間能夠隨便用 JS 讀寫,並無深拷貝的困擾。不過,WASM 只有 int 和 float 之流的數據類型,連 string 都沒有,所以對於稍複雜一點的對象,都很難手寫出 JS 和 WASM 兩邊各自的結構。如今這件髒活是交由 wasm-bindgen 等輪子來作的。但畢竟這個過程並非直接在 JS 的 Runtime 裏嵌入 C / C++ 函數,和傳統編譯到機器碼的 FFI 仍是挺不同的。

而至於不能在 WASM 的 Memory 對象裏表達的 JS 對象,就會遇到一種雙份快樂的問題了。例如如今若是須要頻繁地用 WASM 操做 JS 對象,那麼幾乎必然是影響性能的。這方面典型的坑是基於 WASM 移植的 OpenGL 應用。像 C++ 中的一個 glTexImage2D 函數,目前編譯到 WASM 後就須要先從 WASM 走到 JS 膠水層,再在 JS 裏調 gl.texImage2D 這樣的 WebGL API,最後才能經由 C++ binding 調用到原生的圖形 API。這樣從一層膠水變成了兩層,性能不要說比起原生 C++,能比得上直接寫 JS 嗎?

固然,Mozilla 也意識到了這個問題,所以他們在嘗試如何更好地將 Web IDL(也就是瀏覽器原生 API 的 binding)開放給 WASM,並在這個過程當中提出了 WASM Interface Types 概念:既然 WASM 已是個字節碼的中間層了,那麼幹脆給它約定個能一統全部編程語言運行時類型的 IR 規範吧!不過,這一規範仍是但願主要靠協議化、結構化的深拷貝來解決問題,只有將來的 anyref 類型是能夠傳引用的。anyref 有些像 Unix 裏的文件描述符,這裏就不展開了。

因此,將來也許 WASM 裏會有 DOM 的訪問能力,但目前這畢竟還只是個餅。不要說像 WebGL 的某些擴展已經明確 不受 Web IDL 支持,就算距離把主流的 Web 標準都開放到 WASM 並普及到主流用戶,在 2020 年這個時間節點看來仍是挺遙遠的。

瀏覽器:爲何會變成這樣呢…第一次有了高性能的腳本語言,又兼容了高級的原生語言。兩份快樂重疊在一塊兒。而這兩份快樂,又帶來了更多的快樂。獲得的,本該是像夢境通常的幸福時光…可是,爲何,會變成這樣呢…

前端框架早晚會用 WASM 重寫?

我以爲很難,或者說這件事的投入產出比 (ROI) 未必足夠。由於對於主流的前端應用來講,它們都是 IO 密集而不是計算密集型的,這時 WASM 增長的算力很難成爲瓶頸,反而會增長許多工程上的維護成本。

這方面的一個論據,是 Google 的 JIT-less V8 介紹。V8 在關閉 JIT 後峯值性能下降到了不到原先十分之一的級別(見 QuickJS Benchmark),卻也幾乎不影響刷 YouTube 這種輕度應用的性能表現, 在模擬重度 Web 應用負載的 Speedometer 標準下,其跑分也有原先的 60% 左右,只有在 Webpack 打包類型的任務上出現了數量級的差別。你以爲遷移到 WASM 後,峯值算力就算比如今再翻兩倍,能在事件驅動、IO 密集的 GUI 場景中表現出顛覆性的突破嗎?能說服框架做者們徹底放棄現有的 JS 代碼庫,選用另外一種語言來完全重寫框架嗎?何況 WASM 從長期來看,可都要依賴很多體積足以影響首屏性能的 JS 膠水代碼和 polyfill 呢。

用 WASM 重寫主流 UI 框架,意味着前端須要重度依賴一門徹底不一樣的語言技術棧。你說由於 JVM 比 V8 快,因此 Node 應用就應該用 Java 重寫嗎?我看前端圈裏的政治正確明明是反過來的啊…

我即便是死了,釘在棺材裏了,也要在墓裏,用這腐朽的聲帶喊出:JS 牛逼!!!(被禁言)

WASM 屬於前端生態?

這個我不太承認。要知道,一個 WASM 應用,其編譯工具鏈和依賴庫生態,基本徹底不涉及 JS

2018 年我嘗試編譯過 ffmpeg 到 WASM,這整個過程幾乎和 JS 沒任何關係,重點都集中在搭建 Docker 編譯環境和魔改 Makefile 上了。以我當時的水平,整個流程讓我很是困惑。

後來我在折騰嵌入式 Linux 和安卓的過程當中,順帶搞懂了工具鏈的概念。一個原生應用,須要編譯、彙編和連接過程,才能變爲一個可執行文件。好比個人開發機是 Mac,那麼裝在 macOS 上典型的幾套工具鏈像這樣:

  • 面向 macOS 的工具鏈,是編譯到 macOS 二進制格式的 clang 那一套
  • 面向安卓的工具鏈,是編譯到 ARM 二進制格式的 aarch64-linux-android-gcc 那一套(在 NDK 裏翻翻就知道了)
  • 面向 PSP 的工具鏈,是編譯到 MIPS 二進制格式的 mips-gcc 那一套(名字不必定對啊)
  • 面向 WASM 的工具鏈,是面向 WASM 字節碼格式的 Emscripten 那一套

後面三者都是編譯到其它的平臺,所以叫作交叉編譯

一套支持交叉編譯的工具鏈,會附帶上用於支持目標平臺的一些庫,例如 include 了 <GLES2/gl2.h> 以後,你調用到的 glTexImage2D API 就是動態庫裏提供的。有了動態庫,這個 API 才能在 x86 / ARM / MIPS / WASM 等平臺上一致地跑起來(就像安卓上的 .so 格式)。像 Emscripten 就提供了面向 WASM 平臺,編譯成 JS 格式的一套動態庫。但它只能保證這些 API 能用,性能如何就另說了。它本身也對移植 WebGL 時的性瓶頸提出了不少的 優化建議

因此這裏再重複一遍,編譯 WASM 應用所需的依賴庫和整套工具鏈,幾乎都跟 JS 沒什麼關係。JS 就像機器碼那樣,只是人家工具鏈編譯出來的輸出格式而已。在 JS 開發者看來,這整套東西可能顯得至關突兀。但從原生應用開發者的視角看來,這一切都再正常不過了。

來而不往非禮也。WASM 能夠把其它語言引進來,JS 就不能往外走出去了嗎?除了 Flutter 這種修正主義路線,像我詳細介紹過的 Static TypeScript 和 QuickJS,就是信奉 JS 系的冬馬黨們搞的反向輸出:

說了這麼多,那麼 WASM 的適用場景究竟是什麼呢?如今 WASM 社區大力推廣的提案,如 WASI、多線程、GC 這些,其實都跟 JS 生態關係不大,而是方便把更復雜的原生應用直接搬進 Web 的技術需求。話都說到這份上了,你們還沒看出來嗎?這不就是典型雪菜式的明修棧道,暗度陳倉嘛(笑)

最後總結下,JS 和 WASM 的人設大概各自是這樣的:

  • JS:我先來的,哪裏有瀏覽器,哪裏就是個人主場。雖然有人不喜歡個人脾氣,但我到哪都是一頭黑長直的字符串腳本。追個人引擎有的是,但我始終首先是瀏覽器的忠犬。
  • WASM:我是高嶺之花,瀏覽器內外你們都歡迎我,並且誰都能編譯到我,因此歡迎你們都來用個人二進制格式吧。我雖然很想和 JS 和瀏覽器三我的永遠在一塊兒,但最但願之後跨平臺只要靠個人努力就能夠永遠幸福下去了

WASM:啊…若是 JS 是強類型的男孩子的話就行了。

你以爲明明先來的冬馬 (JS) 和高嶺之花的雪菜 (WASM) 哪一個更對你胃口呢?提醒下某些成天喊着我全都要的白學家,雙份快樂可不是通常人玩得起的噢。

通常來講整個白學家(程序員)羣體裏冬馬黨基數最大,但骨灰級玩家經常是雪菜黨。至於我嘛…你看看我簡介裏的花名叫什麼?

最後上一張合照,冬馬和雪菜都是好樣的!

後記

WASM 固然是個革命性的技術,表明了一種跨平臺的全新方向,尤爲對原生應用開發者來講具有巨大的商業價值。但它對前端來講其實就是個瀏覽器內置的字節碼虛擬機,不是一切性能問題的靈丹妙藥。目前網上很多對它的讚美,在我看來多少有些過譽了。因此建議你們不要盲目跟風,仍是從白學,啊不計算機科學的基礎出發,去判斷一個技術的適用場景和價值在哪吧。

本文配圖均來自純 JS 實現的高性能前端應用 稿定 PS。白學部分純屬娛樂,請你們不要過度解讀(如爲何 JS 的 Logo 是雪菜黃,而 WASM 的 Logo 反而是冬馬紫之類)。個人水平有限,歡迎你們對本文技術觀點更進一步的交流,也但願你們能關注個人「前端隨想錄」這個自由技術專欄~

相關文章
相關標籤/搜索