摘要:的目標是對高級程序中間表示的適當低級抽象,即代碼旨在由編譯器生成而不是由人來寫。表示把源代碼變成解釋器可以運行的代碼所花的時間表示基線編譯器和優化編
WebAssembly 那些事兒 什么是 WebAssembly?
WebAssembly 是除 JavaScript 以外,另一種可以在網頁中運行的編程語言,并且相比之下在某些功能和性能問題上更具優勢,過去我們想在瀏覽器中運行代碼來對網頁中各種元素進行控制,只有 JavaScript 這一種選擇,而如今我們可以將其它語言(C/C++ etc.)編譯成 wasm 格式的代碼在瀏覽器中運行。
WebAssembly 的目標是對高級程序中間表示的適當低級抽象,即 wasm 代碼旨在由編譯器生成而不是由人來寫。
每一種目標匯編語言(x86、ARM etc.)都依賴于特定的機器結構,當我們想要把代碼放到用戶的機器上執行的時候,并不知道目標機器結構是什么樣的,而 WebAssembly 與其他的匯編語言不一樣,它不依賴于具體的物理機器,可以抽象地理解成它是 概念機器的機器語言,而不是實際的物理機器的機器語言,正因為如此 WebAssembly 指令有時也被稱為虛擬指令,它比 JavaScript 代碼更直接地映射到機器碼,同時它也代表了“如何能在通用的硬件上更有效地執行代碼”的一種理念。
目前對于 WebAssembly 支持情況最好的編譯器工具鏈是 LLVM,還有一個易用的工具叫做 Emscripten,它通過自己的后端先把代碼轉換成自己的中間代碼(asm.js),然后再轉化成 WebAssembly,實際上它是基于 LLVM 的一系列編譯工具的集合。
Tip:很多 WebAssembly 開發者用 C 語言或者 Rust 開發,再編譯成 WebAssembly,其實還有其他的方式來開發 WebAssembly 模塊:使用 TypeScript 開發 WebAssembly 模塊,或者直接書寫 WebAssembly 文本 etc.。
WebAssembly 代碼存儲在 .wasm 文件內,這類文件是要瀏覽器直接執行的,因為 .wasm 文件內是二進制文件難以閱讀,為方便開發者查看官方給出對 .wasm 文件的閱讀方法:
把 .wasm 文件通過工具轉為 .wast 的文本格式,開發者可以在一定程度上理解這個 .wast 文件(通過 S- 表達式寫成,類似于 lisp 語言的代碼書寫風格)。
Tip:.wast 文件和 .wasm 文件之間的相互轉化可以通過工具 wabt 實現。為什么 WebAssembly 更快? 一些關于性能的歷史
- JavaScript 于 1995 年問世,它的設計初衷并不是為了執行起來快,在前 10 個年頭它的執行速度也確實不快。
- 緊接著,瀏覽器市場競爭開始激烈起來。
- 廣為流傳的“性能大戰”在 2008 年打響,許多瀏覽器引入 JIT 編譯器,JavaScript 代碼的運行速度漸漸變快(10倍!),這使得 JavaScript 的性能達到一個轉折點。
知識遷移:Javascript JIT 工作原理在代碼的世界中,通常有兩種方式來翻譯機器語言:解釋器和編譯器。
如果是通過解釋器,翻譯是一行行地邊解釋邊執行
編譯器是把源代碼整個編譯成目標代碼,執行時不再需要編譯器,直接在支持目標代碼的平臺上運行
解釋器啟動和執行的更快,我們不需要等待整個編譯過程完成就可以運行代碼,從第一行開始翻譯就可以依次繼續執行。正是因為這個原因,解釋器看起來更加適合 JavaScript,對于一個 Web 開發人員來講,能夠快速執行代碼并看到結果是非常重要的。可是當我們運行同樣的代碼一次以上的時候,解釋器的弊處就顯現出來:比如執行一個循環,那解釋器就不得不一次又一次的進行翻譯,這是一種效率低下的表現。
編譯器的問題則恰好相反:它需要花一些時間對整個源代碼進行編譯,然后生成目標文件才能在機器上執行。對于有循環的代碼執行的很快,因為它不需要重復的去翻譯每一次循環。
另外一個不同是,編譯器可以用更多的時間對代碼進行優化,以使的代碼執行的更快;而解釋器是在 runtime 時進行這一步驟的,這就決定它不可能在翻譯的時候用很多時間進行優化。
為了解決解釋器的低效問題,后來的瀏覽器把編譯器也引入進來,形成混合模式。不同的瀏覽器實現這一功能的方式不同,不過其基本思想是一致的:在 JavaScript 引擎中增加一個監視器(也叫分析器),監視器監控著代碼的運行情況,記錄代碼一共運行多少次、如何運行等信息。
起初,監視器監視著所有通過解釋器的代碼,如果同一行代碼運行多次,這個代碼段就被標記成 “warm”,如果運行很多次則被標記成 “hot”。
如果一段代碼變成 “warm”,那么 JIT 就把它送到編譯器去編譯,并且把編譯結果存儲起來。
代碼段的每一行都會被編譯成一個“樁”(stub),同時給這個樁分配一個以“行號 + 變量類型”的索引,如果監視器監視到執行同樣的代碼和同樣的變量類型,那么就直接把這個已編譯的版本 push 出來給瀏覽器。通過這樣的做法可以加快執行速度,但是正如前我所說的,編譯器還可以找到更有效地執行代碼的方法(優化)。
基線編譯器可以做一部分這樣的優化,不過基線編譯器優化的時間不能太久,因為會使得程序的執行在這里 hold 住,不過如果代碼確實非常 “hot”(也就是說幾乎所有的執行時間都耗費在這里),那么花點時間做優化也是值得的。
如果一個代碼段變得 “very hot”,監視器會把它發送到優化編譯器中,生成一個更快速和高效的代碼版本出來,并且存儲之。為了生成一個更快速的代碼版本,優化編譯器必須做一些假設:例如它會假設由同一個構造函數生成的實例都有相同的形狀,就是說所有的實例都有相同的屬性名,并且都以同樣的順序初始化,那么就可以針對這一模式進行優化。
整個優化器起作用的鏈條是這樣的,監視器從他所監視代碼的執行情況做出自己的判斷,接下來把它所整理的信息傳遞給優化器進行優化,如果某個循環中先前每次迭代的對象都有相同的形狀,那么就可以認為它以后迭代的對象的形狀都是相同的,可是對于 JavaScript 從來就沒有保證這么一說,前 99 個對象保持著形狀,可能第 100 個就減少某個屬性。
正是由于這樣的情況,編譯代碼需要在運行之前檢查其假設是不是合理的,如果合理那么優化的編譯代碼會運行,如果不合理那么 JIT 會認為這是一個錯誤的假設,并且把優化代碼丟掉,這時執行過程將會回到解釋器或者基線編譯器,這一過程叫做去優化。
通常優化編譯器會使得代碼變得更快,但是一些情況也會引起一些意想不到的性能問題。如果代碼一直陷入優化去優化的怪圈,那么程序執行將會變慢,還不如基線編譯器快。大多數的瀏覽器限制當優化去優化循環發生的時候會嘗試跳出這種循環,比如如果 JIT 反復做 10 次以上的優化并且又丟棄的操作,那么就不繼續嘗試去優化這段代碼。
JavaScript 所使用的動態類型體系在運行時需要進行額外的解釋工作,例如下面代碼:
function arraySum(arr) { var sum = 0; for (var i = 0; i < arr.length; i++) { sum += arr[i]; } }
+= 循環中這一步看起來很簡單,只需要進行一步計算,但是恰恰因為是用動態類型,所需要的步驟要比我們所想象的更復雜一些:我們假設 arr 是一個有 100 個整數的數組,當代碼被標記為 “warm” 時,基線編譯器就為函數中的每一個操作生成一個樁,sum += arr[i]會有一個相應的樁,并且把里面的 += 操作當成整數加法,但是 sum 和 arr[i] 兩個數并不保證都是整數,因為在 JavaScript 中類型都是動態類型,在接下來的循環當中 arr[i] 很有可能變成了string 類型,整數加法和字符串連接是完全不同的兩個操作,會被編譯成不同的機器碼。
JIT 處理這個問題的方法是編譯多基線樁:如果一個代碼段是單一形態的(即總是以同一類型被調用),則只生成一個樁:如果是多形態的(即調用的過程中,類型不斷變化),則會為操作所調用的每一個類型組合生成一個樁。這就是說 JIT 在選擇一個樁之前會進行多分枝選擇(類似于決策樹),問自己很多問題才會確定最終選擇哪個。
正是因為在基線編譯器中每行代碼都有自己的樁,所以 JIT 在每行代碼被執行的時候都會檢查數據類型,在循環的每次迭代 JIT 也都會重復一次分枝選擇。
如果代碼在執行的過程中 JIT 不是每次都重復檢查的話,那么執行的還會更快一些,而這就是優化編譯器所需要做的工作之一。在優化編譯器中,整個函數被統一編譯,這樣的話就可以在循環開始執行之前進行類型檢查。
一些瀏覽器的 JIT 優化更加復雜:在 Firefox 中給一些數組設定特定的類型,比如數組里面只包含整型,如果 arr 是這種數組類型,那么 JIT 就不需要檢查 arr[i] 是不是整型,這也意味著 JIT 可以在進入循環之前進行所有的類型檢查。
- 隨著性能的提升 JavaScript 可以應用到更多領域(Node.js etc.)
- 通過 WebAssembly 我們很有可能正處于第二個拐點!
當前的 JavaScript 性能如何?下圖片介紹 JS 引擎性能使用的大概分布情況,各個部分所花的時間取決于頁面所用的 JavaScript 代碼,其比例并不代表真實情況下的確切比例情況,并且這些任務并不是離散執行或者按固定順序依次執行的,而是交叉執行:比如某些代碼在進行解析時,其他一些代碼正在運行而另一些正在編譯。
Parsing:表示把源代碼變成解釋器可以運行的代碼所花的時間;
Compiling + optimizing:表示基線編譯器和優化編譯器所花的時間(某些優化編譯器的工作并不在主線程運行)
Re-optimizing:當 JIT 發現優化假設錯誤,丟棄優化代碼所花的時間(包括重優化的時間、拋棄并返回到基線編譯器的時間)
Execution:執行代碼的時間
Garbage collection:垃圾回收、清理內存的時間
早期的 JavaScript 執行類似于下圖,各個過程順序進行:
各個瀏覽器處理下圖中不同的過程有著細微的差別,我們使用 SpiderMonkey 作為模型來講解不同的階段:
文件獲取這一步并沒有顯示在圖表中,但是看似簡單的從服務器獲取文件得這個步驟,卻會花費很長時間,WebAssembly 比 JavaScript 的壓縮率更高,即在服務器和客戶端之間傳輸文件更快,尤其在網絡不好的情況下。
解析當文件到達瀏覽器時 JavaScript 源代碼就被解析成抽象語法樹,瀏覽器采用懶加載的方式進行,只解析真正需要的部分,而對于瀏覽器暫時不需要的函數只保留它的樁。解析過后 AST(抽象語法樹)就變成了中間代碼(字節碼:一種中間代碼,通過虛擬機轉換為機器語言)提供給 JS 引擎編譯,而 WebAssembly 則不需要這種轉換,因為它本身就是中間代碼,要做的只是解碼并且檢查確認代碼沒有錯誤即可。
抽象語法樹(Abstract Syntax Tree)也稱為 AST 語法樹,指的是源代碼語法所對應的樹狀結構。
程序代碼本身可以被映射成為一棵語法樹,而通過操縱語法樹我們能夠精準的獲得程序代碼中的某個節點。Espsrima 提供一個在線解析的工具,我們可以借助于這個工具將 JavaScript 代碼解析為一個 JSON 文件表示的樹狀結構,舉例如下所示:
// Life, Universe, and Everything var answer = 6 * 7;
{ "type": "Program", "body": [ { "type": "VariableDeclaration", "declarations": [ { "type": "VariableDeclarator", "id": { "type": "Identifier", "name": "answer" }, "init": { "type": "BinaryExpression", "operator": "*", "left": { "type": "Literal", "value": 6, "raw": "6" }, "right": { "type": "Literal", "value": 7, "raw": "7" } } } ], "kind": "var" } ], "sourceType": "script" }
抽象語法樹的作用非常的多,比如編譯器、IDE、壓縮優化代碼 etc.,在 JavaScript 中雖然我們并不會常常與 AST 直接打交道,但卻也會經常涉及到它的使用:例如使用 UglifyJS 來壓縮代碼時,這背后的原理就是在對 JavaScript 的抽象語法樹進行操作。
編譯和優化JavaScript 是在代碼的執行階段編譯的,因為它是弱類型語言,當變量類型發生變化時,同樣的代碼會被編譯成不同版本,不同瀏覽器處理 WebAssembly 的編譯過程也不同,有些瀏覽器只對 WebAssembly 做基線編譯,而另一些瀏覽器用 JIT 來編譯,不論哪種方式,WebAssembly 都更貼近機器碼所以它更快:
在編譯優化代碼之前不需要提前運行代碼以知道變量都是什么類型
編譯器不需要對同樣的代碼做不同版本的編譯
很多優化在 LLVM 階段就已經完成
重優化有些情況下 JIT 會反復地進行拋棄優化代重優化過程,當 JIT 在優化假設階段做的假設在執行階段發現是不正確的時候,就會發生這種情況:比如當循環中發現本次循環所使用的變量類型和上次循環的類型不一樣,或者原型鏈中插入了新的函數,都會使 JIT 拋棄已優化的代碼。
需要花時間丟掉已優化的代碼并且回到基線版本
如果函數依舊頻繁被調用,JIT 可能會再次把它發送到優化編譯器又做一次優化編譯
而在 WebAssembly 中類型都是確定的,所以 JIT 不需要根據變量的類型做優化假設,也就是說 WebAssembly 沒有重優化階段。
執行開發人員自己也可以寫出執行效率很高的 JavaScript 代碼,這需要了解 JIT 的優化機制,例如要知道什么樣的代碼編譯器會對其進行特殊處理,然而大多數的開發者是不知道 JIT 內部的實現機制的,即使知道 JIT 的內部機制也很難寫出符合 JIT 標準的代碼,因為人們通常為了代碼可讀性更好而使用的編碼模式恰恰不合適編譯器對代碼的優化;加之 JIT 會針對不同的瀏覽器做不同的優化,所以對于一個瀏覽器優化的比較好,很可能在另外一個瀏覽器上執行效率就比較差。
正是因為這樣執行 WebAssembly 通常會比較快,很多 JIT 為 JavaScript 所做的優化在 WebAssembly 并不需要;另外 WebAssembly 就是為編譯器而設計的,開發人員不直接對其進行編程,這樣就使得 WebAssembly 專注于提供更加理想的指令(執行效率更高的指令)給機器即可。
垃圾回收JavaScript 中開發者不需要手動清理內存中不用的變量,JS 引擎會自動地做這件事情即垃圾回收的過程。可是當我們想要實現性能可控,垃圾回收可能就是一個大問題:垃圾回收器會自動開始,這是不受控制的,所以很有可能它會在一個不合適的時機啟動,目前的大多數瀏覽器已經能給垃圾回收安排一個合理的啟動時間,不過這還是會增加代碼執行的開銷。
目前為止 WebAssembly 不支持垃圾回收,內存操作都是手動控制的,這對于開發者來講確實會增加開發成本,不過也使得代碼的執行效率更高。
WebAssembly 的現在與未來 JavaScript 和 WebAssembly 之間調用的中間函數目前在 Javascript 中調用 WebAssembly 的速度比本應達到的速度要慢,這是因為中間需要做一次“蹦床運動”:JIT 沒有辦法直接處理 WebAssembly,所以 JIT 要先把 WebAssembly 函數發送到懂它的地方,這一過程是引擎中比較慢的地方。
按理來講,如果 JIT 知道如何直接處理 WebAssembly 函數,那么速度會有百倍的提升,如果我們傳遞的是單一任務給 WebAssembly 模塊,那么不用擔心這個開銷,因為只有一次轉換,也會比較快,但是如果是頻繁地從 WebAssembly 和 JavaScript 之間切換,那么這個開銷就必須要考慮了。
快速加載JIT 必須要在快速加載和快速執行之間做權衡,如果在編譯和優化階段花了大量的時間,那么執行的必然會很快,但是啟動會比較慢。目前有大量的工作正在研究,如何使預編譯時間和程序真正執行時間兩者平衡。WebAssembly 不需要對變量類型做優化假設,所以引擎也不關心在運行時的變量類型,這就給效率的提升提供了更多的可能性,比如可以使編譯和執行這兩個過程并行。加之最新增加的 JavaScript API 允許 WebAssembly 的流編譯,這就使得在字節流還在下載的時候就啟動編譯。
FireFox 目前正在開發兩個編譯器系統:一個編譯器先啟動,對代碼進行部分優化,在代碼已經開始運行時,第二個編譯器會在后臺對代碼進行全優化,當全優化過程完畢,就會將代碼替換成全優化版本繼續執行。
添加后續特性到 WebAssembly 標準的過程 直接操作 DOM目前 WebAssembly 沒有任何方法可以與 DOM 直接交互,就是說我們還不能通過比如 element.innerHTML 的方法來更新節點。想要操作 DOM 必須要通過 JS,那么就要在 WebAssembly 中調用 JavaScript 函數,不管怎么樣都要通過 JS 來實現,這比直接訪問 DOM 要慢得多,所以這是未來一定要解決的一個問題。
共享內存的并發性提升代碼執行速度的一個方法是使代碼并行運行,不過有時也會適得其反,因為不同的線程在同步的時候可能會花費更多的時間。這時如果能夠使不同的線程共享內存,那就能降低這種開銷,實現這一功能 WebAssembly 將會使用 JavaScript 中的 SharedArrayBuffer,而這一功能的實現將會提高程序執行的效率。
SIMD(單指令,多數據)SIMD(Single Instruction, Multiple Data)在處理存放大量數據的數據結構有其獨特的優勢,比如存放很多不同數據的 vector(容器),就可以用同一個指令同時對容器的不同部分做處理,這種方法會大幅提高復雜計算的效率比如游戲或者 VR 應用。
異常處理許多語言都仿照 C++ 式的異常處理,但是 WebAssembly 并沒有包含異常處理,如果我們用 Emscripten 編譯代碼,就知道它會模擬異常處理,但是這一過程非常之慢,慢到想用 “DISABLEEXCEPTIONCATCHING” 標記把異常處理關掉。如果異常處理加入到 WebAssembly 中那就不必再采用模擬的方式,而異常處理對于開發者來講又特別重要,所以這也是未來的一大功能點。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/110133.html
摘要:對于很多沒有中間語言的字節碼的編程語言來說,根本不存在解釋執行與編譯執行的選項,比如傳統只能編譯執行,直接將代碼編譯成為可執行的二進制機器碼,我們電腦上文件就是編譯的成果。 Daniel Larimer 在最近的博客中透露,EOS 新增了官方的 WebAssembly 解釋器,用來解釋執行 WebAssembly 智能合約,加上之前的編譯執行,EOS 智能合約有了兩種執行方式。 對于很...
摘要:抽象語法樹大致流程生成然后通過類型斷言進行相應的轉換反編譯工具全集小程序推薦逆向反編譯四大工具利器年支持的反編譯工具匯總原文 像軟件加密與解密一樣,javascript的混淆與解混淆同屬于同一個范疇。道高一尺,魔高一丈。沒有永恒的黑,也沒有永恒的白。一切都是資本市場驅動行為,現在都流行你能為人解決什么問題,這個概念。那么市場究竟能容納多少個能解決這種問題的利益者。JS沒有秘密。 其實本...
摘要:前端日報精選帶來哪些新特性一之中的無狀態和有狀態組件譯使用柵格和打造布局與聯合發布中文譯學習個常見錯誤阻礙你進步掘金里的新玩意知乎專欄介紹掘金壓測方案之簡介那些事兒新技術全拆解剖析個人文章和項目開發商城前端課堂 2017-09-15 前端日報 精選 Node.js 8.5 帶來哪些新特性CSS Masonry Layouts【一】之 multi-columnsReact中的無狀態和有狀...
摘要:在當前階段,僅僅只是字節碼規范。如果都沒有將代碼編譯為字節碼的工具,要起步就很困難了。接下來要做的是使用將格式的代碼轉換為二進制碼。運行文件,最后就能得到瀏覽器需要的真正的二進制碼。 本文轉載自:眾成翻譯譯者:文藺鏈接:http://www.zcfy.cc/article/1031原文:http://cultureofdevelopment.com/blog/build-your-fi...
摘要:本文是圖說系列文章的第五篇。這樣的話,使用的開發者也不需要做任何適配,但是它們卻能獲得更高性能。該圖并不是用來準確的衡量其性能的。運行編寫出高性能的代碼是可能的。這種清理工作由引擎自動進行,稱為垃圾回收。 本文是圖說 WebAssembly 系列文章的第五篇。如果您還未閱讀之前的文章,建議您從第一篇入手。 在上一篇文章中,我們說到了使用 WebAssembly 和 JavaScript...
閱讀 1322·2023-04-26 01:28
閱讀 2075·2021-11-08 13:28
閱讀 2322·2021-10-12 10:17
閱讀 2302·2021-09-28 09:46
閱讀 4147·2021-09-09 09:33
閱讀 3728·2021-09-04 16:40
閱讀 1102·2019-08-29 15:21
閱讀 2696·2019-08-26 17:17