首先,我們需要知道編譯和直譯並不是程式語言的特性,而是關於程式的執行方式。 理論上,任何程式語言都能是編譯式或直譯式,這取決於底層是用編譯器 (compiler) 或直譯器 (interpreter) 來決定,兩者混用的執行方式也很常見,例如先編譯再直譯的程式語言 Java,或先直譯再編譯的及時編譯 (Just-in-time compilation, JIT) 技術。

第二,我們還要知道在電腦中是由 CPU 來執行程式,但 CPU 只能看懂機器語言,沒辦法直接理解我們用英文撰寫的程式碼。 因此,無論是編譯語言或直譯語言,要讓電腦執行程式碼都一定有一個將程式碼翻譯成低階語言的步驟,只是編譯語言是在程式執行之前編譯,而直譯語言是在程式執行時轉譯。

通常會將編譯器的執行動作稱為編譯,而將直譯器的執行動作稱為轉譯,但兩者都是用來將高階程式語言轉為低階語言。

名詞解釋

本文涉及諸多電腦科學名詞,很多名詞經常混淆,我也不敢保證我的理解都是正確的,本節先統一整理這些名詞,並說明其含義。如果有誤,歡迎大家留言讓我知道。

機器語言 (machine language)
一種使用二進位表示的指令集體系,可由 CPU 直接執行。
機器碼 (machine code)
指使用機械語言寫出來的程式碼本身,類似「程式語言」與「程式碼」的概念。
原生碼 (Native Code)
機器碼有時也被稱為原生碼,但原生碼更強調程式語言與執行平台相關的部份。
運算碼 (Opcode)
用來告訴 CPU 需要執行哪個指令,並且 Opcode 可以直接由 CPU 執行。但不同的 CPU 擁有不同的指令集架構,因此同一份 Opcode 不一定能在不同的機器上執行。
位元組碼 (Bytecode)
用來告訴 CPU 需要執行哪個指令,但 Bytecode 更像是軟體指令集,沒辦法直接在 CPU 上接執行,而必需透過虛擬機器來執行,因此 Bytecode 通常具備跨平台執行能力。此外,Bytecode 可以被拆為多個 Opcode。
組合語言 (assembly language)
一種低階的程式語言,組合語言必需通過組譯過程轉換為機械碼。
組譯 (Assemble)
也是一種編譯過程,只是強調是將組合語言轉為機器碼。
中間語言 (Intermediate language)
原本指的是編譯器將原始碼編譯為目的碼過程中,所使用的中間標示。但現在中間語言也包含編譯器或直譯器從原始碼轉換為機械碼的過程中所使用的過渡程式語言,例如:Opcode 或 Bytecode。
中介碼 (Intermediate representation, IR)
中介碼是一種資料結構,專指編譯器將原始碼編譯為目的碼過程中所使用的中間標示,常見的結構有 Linear IR、Graphical IR 及 Hybrid IR。使用直譯器產生的中間語言,較少稱為中介碼。
編譯 (Compile)
編譯是將一種程式語言轉為另一種程式語言的動作。所以在直譯器中,也會有編譯的動作,例如:PHP 直譯器,會將 PHP 程式碼編譯成 Opcode。
編譯器 (compiler)
編譯器是將一種程式語言轉為另一種程式語言的程式,編譯器中不只有編譯程式,也包含語法檢查、組譯...等其他程式。
目的碼 (Object code)
目的碼是指編譯這個動作的產物,也就是泛指轉換後的程式語言。目的碼可以是機器碼,也可以是位元組碼或任何其它程式語言。
虛擬機器 (Virtual machine)
虛擬機器分為不同的類型,例如系統虛擬機器 (如 VirtualBox 或 VMware),以及程式虛擬機器 (如 JVM)。在本文中,提到的虛擬機器都是指程式虛擬機器。

編譯語言 (Compiled language)

常見的編譯策略有兩種:預先編譯 (Ahead-of-time compilation, AOT) 和及時編譯 (Just-in-time compilation, JIT)。 編譯語言通常是指採用 AOT 的程式語言,採用 JIT 的程式語言不一定是編譯語言。

在 AOT 模式中,強調在程式執行之前,先將全部程式碼經過編譯,並且必需帶來效能的優化。 大部份 AOT 模式都是將程式碼編譯成機器碼,而機器碼帶有作業系統及硬體資訊,也就是説同一份程式碼在 A 平台編譯,是無法直接在 B 平台執行,必須將程式碼複製到 B 平台重新編譯。

AOT 強調必需帶來效能優化,因此將 Java 編譯成 Java Bytecode 的動作,雖然被稱為編譯,但很少稱為 AOT,這個過程主要是為了讓 Java 在 JVM 上執行的一個必備過程,而不是為了效能優化。儘管如此,我們仍然會把編譯 Java 程式碼的 javac 稱為編譯器。

C++ 是 AOT 的典型例子,C++ 的程式碼副檔名為 .cpp,執行前必須先編譯為 .exe,最後實際執行的是 .exe 檔案,但這個 .exe 檔是不能直接複製到 Linux 作業系統執行。

C++ 執行流程
Java 執行流程

直譯語言 (Interpreted language)

直譯語言是將高階程式語言轉譯為中間語言,再透過虛擬機器執行,而實現整個轉譯流程的程式通常稱為引擎,例如 PHP 的 Zend Engine 或 JavaScript 的 V8 Engine (V8 使用 JIT)。 引擎是由直譯器與虛擬機器組成,當程式執行時,直譯器會將程式碼轉譯成中間語言 (通常為 Opcode 或 Bytecode),再由虛擬機器執行。 由於每次執行都需要轉譯,為了提升執行速度,通常會使用快取機制來減少直譯器轉譯次數。

直譯語言具備跨平台執行能力,只要更換引擎,就能夠直接執行程式碼,即使經過修改也無需重新編譯。 此外,大多數直譯語言都支援互動式編程,例如:Python、JavaScript。 然而,直譯語言最大問題就是執行速度較慢,由於每次執行都必須重新轉譯才能執行,即使使用快取機制,執行速度仍然比不上編譯語言。

PHP 是直譯語言的典型例子,PHP 在執行時,會先使用 Zend Engine 中的直譯器將 PHP 程式碼轉譯為抽象語法樹 (Abstract Syntax Tree, AST),再將 AST 轉換為 Opcode,最後由 Zend VM 執行這些 Opcode。

直譯語言執行流程

逐行編譯執行

有時候,您可能會聽到直譯器是逐行編譯逐行執行,但事實上,直譯器仍然可以整份程式碼解析完再執行,這取決於您如何使用直譯器。

舉例來說,以下是一段有問題的 Python 程式碼,如果您使用互動模式逐行輸入並執行,最終畫面會輸出 Hello 文字,並在執行第 2 行時出現錯誤。 但如果您使用指令直接執行整份 Python 程式碼,您會發現連 Hello 都不會輸出,直接在第 2 行出現錯誤。

                
                    print("Hello")
                    2var = 'World'
                    print(2var)
                
            
逐行執行,輸出 Hello
整份執行,不會輸出 Hello

及時編譯 (Just-in-time compilation, JIT)

JIT 經常在直譯語言中,用來將中間語言編譯成機器碼,以加速程式運行。但在非常少見的狀況,也有從高階程式語言直接編譯成機器碼的狀況,例如早期的 V8 Engine。

在直譯語言中,每次執行都需要將程式碼轉譯成中間語言 (Opcode 或 Bytecode),再讓虛擬機器執行中間語言。 而 JIT 編譯器,是進一步將部份中間語言編譯為機器碼,並且繞過虛擬機器直接由 CPU 執行。 由於 JIT 在編譯過程中存在延遲現象,為了改善這個問題,JIT 還會快取編譯完成後的機器碼,以減少編譯次數。

JIT 在大部分的情況下都比直譯模式速度更快,在少數狀況下甚至比 AOT 模式更好,因為有些優化方式,僅能在執行時才有辦法進行。

及時編譯執行流程

總結

預先編譯 直譯 及時編譯
執行方式 在程式執行之前,使用編譯器將程式碼編譯成機器語言,再透過 CPU 執行。 在程式執行時,由直譯器將程式碼轉譯成中間語言,再透過虛擬機器執行。 在程式執行時,將部份中間語言編譯為機器碼,再透過 CPU 執行。
執行速度
優點
  • 執行速度快
  • 可在編譯時,提早發現一些錯誤
  • 編譯後,無法看到原始碼,避免原始碼外流
  • 執行時,使用更少的記憶體
  • 跨平台執行
  • 程式碼修改後,可快速測試
  • 大部分都提供互動式撰寫程式碼
  • 與直譯語言類似,但改善執行速度
缺點
  • 編譯後,無法跨平台執行
  • 每次修改都需重新編譯
  • 執行速度慢
  • 原始碼保護較弱
案例 C
C++
Go
Swift
大部分為靜態語言
JavaScript
PHP
Python
Java
大部分主流直譯語言
編譯語言雖然會在執行前將原始碼編譯成機械碼,但並不能把它當做原始碼保護的手段,真正遇到高手,還是有辦法把機械碼反組譯回來,並進行修改。 只是比起直譯語言,編譯語言對原始碼還是有比較高的保護。