JavaScript是直譯語言--V8、JIT

2021-05-26 09:00:01

程式語言

可以通過」語言「來控制計算機,讓計算機為我們做事情。(類似於中文、英文)

程式語言是用來控制計算機的一系列指令(Instruction),它有固定的格式和詞彙(不同程式語言的格式和詞彙不一樣),必須遵守,否則就會出錯,達不到我們的目的。

程式語言的發展大概經歷了以下幾個階段: 組合語言 ==> 程式導向程式設計 ==> 物件導向程式設計

  • 組合語言是程式語言的拓荒年代,它非常底層,直接和計算機硬體打交道,開發效率低,學習成本高;
  • C語言是程式導向的程式語言,已經脫離了計算機硬體,可以設計中等規模的程式了;
  • Java、C++、Python、C#、PHP 等是物件導向的程式語言,它們在程式導向的基礎上又增加了很多概念。

在這裡插入圖片描述

程式語言的從執行原理上分為兩類:直譯語言和編譯型語言

計算機不能直接理解機器語言以外的語言,因此需要將我們寫的程式碼編譯成機器語言,然後再交給計算機去執行。

編譯型語言

程式在執行之前需要一個專門的編譯過程,把程式編譯為機器語言的檔案,執行時不需要重新翻譯,直接使用編譯的結果就行了。程式執行效率高,依賴編譯器,跨平臺性差些。如C、C++、Delphi等。

直譯語言

程式不需要編譯,程式在執行時才翻譯成機器語言,每執行一次都要翻譯一次。因此效率比較低。如 Python、Shell、JavaScript 等。

Java 語言

編譯器(javac)把原始碼轉化為位元組碼,然後直譯器(Java.exe)把位元組碼轉換為計算機理解的機器碼來執行。其中編譯器和直譯器都是 Java 虛擬機器器(JVM)的一部分,由於針對不同的硬體與OS,Java 直譯器有所不同,因此可以實現「一次編譯、到處執行」。所以 JVM 是Java 跨平臺特性的關鍵所在 – 引入 JVM 後,Java 語言在不同平臺上執行時不再需要重新編譯。

對於前端開發同學使用的 JavaScript 語言,屬於典型的直譯語言

JavaScript

JavaScript 作為程式語言的一種,直接輸送給計算機(CPU)是不認識的(上面有提及),需要將其轉換為指令集。不同型別的 CPU 的指令集是不一樣的。JavaScirpt 引擎可以將 JavaScript 程式碼編譯為不同 CPU(Intel, ARM 以及 MIPS 等)對應的機器碼,同時引擎還可以執行程式碼、分配記憶體以及垃圾回收等。

Google V8 是開源高效能 JavaScript 和 WebAssembly 引擎,被用於 Chrome 和 Node.js 等。其中包括重要的四個模組:

  1. Parser:將 JavaScript 原始碼轉換為 Abstract Syntax Tree (AST);
  2. Ignition:直譯器,將 AST 轉換為 Bytecode,解釋執行 Bytecode;同時收集 TurboFan 優化編譯所需的資訊,比如函數引數的型別;
  3. TurboFan:編譯器,利用Ignitio所收集的型別資訊,將Bytecode轉換為優化的組合程式碼(計算機可識別);
  4. Orinoco:垃圾回收,負責將程式不再需要的記憶體空間回收。

整個轉換過程:JavaScript ==> AST ==> Bytecode ==> Machine Code

關於 v8 引擎是如何工作的,可以看 這篇文章

V8 出現之前,所有的 JavaScript 虛擬機器器所採用的都是解釋執行的方式,這是 JavaScript 執行速度過慢的主要原因之一。而 V8 率先引入了即時編譯(JIT)雙輪驅動的設計(混合使用編譯器和直譯器的技術),這是一種權衡策略,給 JavaScript 的執行速度帶來了極大的提升。

絕大多數編譯器以預先編譯(AOT)或實時編譯(JIT)形式工作。

  • 使用命令列或者整合式開發環境(IDE)呼叫預先編譯(AOT)的編譯器,如 gcc
  • 實時編譯器通常是用來提高效能的,令你沒有感知的,如 V8

即時編譯 JIT(Just-in-time)

直譯器的工作方式:邊解釋,邊執行。 對於迴圈等會存在解釋多次的情況。從而導致執行速度變慢。

for (let i = 0; i < len; i++) {
  doSomething(i)
}

整體來說,為了解決直譯器的低效問題,後來的瀏覽器把編譯器也引入進來,形成混合模式。最終,結合了直譯器和編譯器的兩者優點。

They added a new part to the JavaScript engine, called a monitor (aka a profiler). That monitor watches the code as it runs, and makes a note of how many times it is run and what types are used.

關於 JIT 的原理,大部分來自 這篇文章,英文好的同學可自行跳轉查閱。

基本思想: 在 JavaScript 引擎中增加一個監視器(也叫分析器)。監視器(monitor)監控著程式碼的執行情況,記錄程式碼一共執行了多少次、如何執行的等資訊。後續遇到相同程式碼時,跳過解釋,直接執行。

執行步驟

第一步:Interpreter

使用直譯器執行,當某一行程式碼被執行了幾次,這行程式碼會被打上 Warm 的標籤;當某一行程式碼被執行了很多次,這行程式碼會被打上 Hot 的標籤

第二步:Baseline compiler

被打上 Warm 標籤的程式碼會被傳給 Baseline Compiler 編譯且儲存,同時按照行數 (Line number)變數型別 (Variable type) 被索引。當發現執行的程式碼命中索引,會直接取出編譯後的程式碼給瀏覽器執行,從而不需要重複編譯已經編譯過的程式碼。

第三步:Optimizing compiler

被打上 Hot 標籤的程式碼會被傳給 Optimizing compiler,這裡會對這部分帶碼做更優化的編譯(型別假設)。在執行前會做型別檢查,看是假設是否成立,如果不成立執行就會被打回 interpreter 或者 baseline compiler 的版本,這個操作叫做 「去優化」。

JIT 會增加多餘的開銷:

  • 優化和去優化開銷
  • 監視器記錄資訊對記憶體的開銷
  • 發生去優化情況時恢復資訊的記錄對記憶體的開銷
  • 對基線版本和優化後版本記錄的記憶體開銷

所以,整體來看是一個空間換時間的優化方案。當然,通過上述三個步驟,可得知,雖然 JavaScript 是弱型別語言,隨意修改變數的型別會導致 JIT 編譯效率下降(命中索引概率低)。

說在後面

對於整個直譯語言及現有的相關優化方式(JIT)瞭解之後,對於後續文章要提到的 esbuild 會有更好的理解。esbuild 也被稱為下一代構建工具(使用 Go 語言編寫,基於 ESM)。coming soon~~

esbuild:An extremely fast JavaScript bundler
Our current build tools for the web are 10-100x slower than they could be. The main goal of the esbuild bundler project is to bring about a new era of build tool performance, and create an easy-to-use modern bundler along the way.