程式語言簡介/解釋型程式
直譯器以不同的方式執行程式。它們不生成本機二進位制程式碼,至少通常情況下不生成。相反,直譯器將程式轉換為中間表示,通常是樹,並使用演算法遍歷這棵樹來模擬每個節點的語義。在上一章中,我們用 Prolog 實現了一個小型直譯器,用於一種程式語言,該語言的程式表示算術表示式。即使那是一個非常簡單的直譯器,它也包含了解釋過程的所有步驟:我們有一個表示程式語言抽象語法的樹,以及一個 訪問者 遍歷這棵樹,執行一些與解釋相關的任務。
源程式在直譯器中以其原始格式毫無意義,例如,一系列 ASCII 字元。因此,與編譯器類似,直譯器必須解析源程式。但是,與編譯器不同,直譯器不需要在執行程式之前解析所有原始碼。也就是說,只有程式執行流可以訪問的程式文字部分需要被翻譯。因此,直譯器做了一種延遲翻譯。
直譯器相對於編譯器的主要優勢是可移植性。如前所述,編譯器生成的二進位制程式碼是專門針對目標計算機體系結構定製的。另一方面,直譯器直接處理原始碼。隨著 全球資訊網 的興起,以及從遠端伺服器下載和執行程式的可能性,可移植性成為一個非常重要的問題。因為 客戶端 Web 應用程式必須在許多不同的機器上執行,因此瀏覽器下載遠端軟體的二進位制表示並不有效。相反,必須提供原始碼。
編譯後的程式通常比解釋後的程式執行速度快,因為編譯後的程式和底層硬體之間存在更少的中間環節。但是,我們必須牢記,編譯程式是一個漫長的過程,正如我們之前所見。因此,如果程式只打算執行一次,或者最多執行幾次,那麼解釋它可能比編譯並執行它更快。這種型別的場景在客戶端 Web 應用程式中很常見。例如,JavaScript 程式通常被解釋,而不是編譯。這些程式從遠端 Web 伺服器下載,並且一旦瀏覽器會話過期,它們的程式碼通常就會丟失。
更改程式的原始碼是應用程式開發過程中的常見任務。使用編譯器時,每次更改都意味著潛在的長時間等待。編譯器需要翻譯修改後的檔案,並連結所有二進位制檔案以建立一個可執行程式,然後再執行該程式。程式越大,這種延遲就越長。另一方面,由於直譯器不會在執行之前翻譯所有原始碼,因此測試修改所需的時間明顯更短。因此,直譯器傾向於有利於軟體 原型 的開發。
示例:bash 指令碼: bash 指令碼 是一個典型的直譯器,常用於 Linux 作業系統。該直譯器為使用者提供一個 命令列介面,也就是說,它為使用者提供一個 提示符,使用者可以在其中輸入命令。這些命令被讀取並解釋。命令也可以組合在一個檔案中。一個bash 指令碼是一個檔案,其中包含要由 bash shell 執行的命令列表。Bash 是一種 指令碼 語言。換句話說,bash 使使用者能夠非常容易地呼叫用 bash 本身之外的其他程式語言實現的應用程式。這樣的指令碼可以用來自動執行使用者經常需要執行的一系列命令。以下幾行是可以透過指令碼檔案儲存的非常簡單的命令,例如名為 my_info.sh 的檔案
#! /bin/bash
# script to present some information
clear
echo 'System date:'
date
echo 'Current directory:'
pwd
指令碼中的第一行(#! /bin/bash)指定了應該使用哪個 shell 來解釋指令碼中的命令。通常,作業系統會提供多個 shell。在本例中,我們使用 bash。第二行(# script to present some information)是 註釋,在執行指令碼時沒有任何效果。bash 指令碼的生命週期比 C 程式的生命週期簡單得多。可以使用文字編輯器(如 vim)編輯指令碼檔案。之後,需要更改其在 Linux 檔案系統中的 許可權,以便使其可執行。可以透過在檔名之前加上其在 檔案系統 中的位置來執行指令碼呼叫。因此,使用者可以透過在 shell 中鍵入 "path/my_info.sh" 來執行指令碼,其中 path 表示找到指令碼所需的路徑
$> ./my_info.sh System date: Seg Jun 18 10:18:46 BRT 2012 Current directory: /home/IPL/shell
虛擬機器 是在軟體中模擬的硬體。它將直譯器、執行時支援系統和解釋程式碼可以使用的庫集合組合在一起。通常,虛擬機器解釋類似彙編的程式表示。因此,虛擬機器彌合了編譯器和直譯器之間的差距。編譯器轉換程式,將其從高階語言轉換為低階 位元組碼。然後,這些位元組碼被虛擬機器解釋。
虛擬機器的最重要的目標之一是可移植性。虛擬化的程式直接由虛擬機器執行,這樣程式開發者就可以忽略虛擬機器執行的硬體。例如,Java 程式是虛擬化的。事實上,Java 虛擬機器 (JVM) 可能是當今使用最廣泛的虛擬機器。任何支援 Java 虛擬機器的硬體都可以執行 Java 程式。在這種情況下,虛擬機器確保所有不同的程式都具有相同的語義。描述 Java 程式這一特性的口號是 "一次編寫,隨處執行"。這個口號說明了 Java 的 跨平臺 優勢。為了保證這種一致的行為,每個 JVM 都附帶一個非常大的軟體庫,即 Java 應用程式程式設計介面。該庫的部分內容由編譯器以特殊方式處理,並在虛擬機器級別直接實現。例如,Java [執行緒] 就是以這種方式處理的。
Java 程式語言如今非常流行。Java 執行環境的可移植性是其流行的關鍵因素之一。Java 最初被構想為嵌入式裝置的程式語言。然而,在 Java 釋出時,全球資訊網也正迎來革命性的首次亮相。在 90 年代初期,對能夠下載並在 網路瀏覽器 中執行的程式的需求量很大。Java 憑藉 Java 小程式 填補了這一空白。如今,與 JavaScript 和 Flash 程式等其他替代方案相比,Java 小程式已經不再受寵。然而,當其他技術開始在 Web 應用程式的客戶端流行起來時,Java 已經是世界上使用最廣泛的程式語言之一。而且,在最初的 Web 革命多年之後,世界見證了計算機歷史的新篇章:智慧手機 作為通用硬體的崛起。可移植性再次成為首要因素,而 Java 再次成為這一新興市場的重要角色。 Android 虛擬機器 Dalvik 旨在執行 Java 程式。
即時編譯
[edit | edit source]通常,編譯後的程式執行速度比解釋後的程式快。但是,在某些情況下,解釋程式碼執行速度更快。例如,shootout 基準測試遊戲包含一些比等效 C 程式更快的 Java 基準測試。這種效率背後的核心技術是 即時編譯器,簡稱 JIT。JIT 編譯器在解釋程式時將其轉換為二進位制程式碼。這種設定為 推測性 程式碼最佳化提供了許多可能性。換句話說,JIT 編譯器可以訪問程式正在操作的執行時值;因此,它可以使用這些值來生成更好的程式碼。JIT 編譯器的另一個優點是,它不需要編譯程式的每個部分,而只需要編譯執行流可以訪問的那些部分。即使在這種情況下,直譯器也可能決定僅編譯函式中執行頻率最高的那些部分,而不是整個函式體。
下面的程式提供了一個玩具 JIT 編譯器的示例。如果執行正確,程式將列印 Result = 1234。根據作業系統採用的保護機制,程式可能無法正確執行。特別是,應用了 資料執行保護 (DEP) 的系統將不會執行此程式直到結束。我們的“JIT 編譯器”將一些彙編指令轉儲到一個名為 program 的陣列中,然後將執行轉移到此陣列。
#include <stdio.h>
#include <stdlib.h>
int main(void) {
char* program;
int (*fnptr)(void);
int a;
program = malloc(1000);
program[0] = 0xB8;
program[1] = 0x34;
program[2] = 0x12;
program[3] = 0;
program[4] = 0;
program[5] = 0xC3;
fnptr = (int (*)(void)) program;
a = fnptr();
printf("Result = %X\n",a);
}
通常,JIT 的工作方式類似於上面的程式。它編譯解釋後的程式碼,並將編譯結果(二進位制程式碼)轉儲到一個標記為可執行的記憶體陣列中。然後,JIT 更改直譯器的執行流,使其指向新寫入的記憶體區域。為了讓讀者對 JIT 編譯器有一個大致的瞭解,下圖展示了 Trace Monkey,它是 Mozilla Firefox 瀏覽器用來執行 JavaScript 程式的編譯器之一。
TraceMonkey 是一個 基於跟蹤 的 JIT 編譯器。它不編譯整個函式。相反,它只將函式中最常執行的路徑轉換為二進位制程式碼。TraceMonkey 建立在名為 SpiderMonkey 的 JavaScript 直譯器之上。SpiderMonkey 解釋位元組碼。換句話說,JavaScript 原始檔被轉換為一系列類似彙編的指令,這些指令由 SpiderMonkey 解釋。直譯器還會監控執行頻率較高的程式路徑。當某個程式路徑達到一定的執行閾值後,它將被轉換為機器程式碼。這種機器程式碼被稱為跟蹤,即一系列線性指令。然後,跟蹤透過 nanojit(Tamarin JavaScript 引擎中使用的 JIT 編譯器)轉換為原生代碼。當該跟蹤的執行結束時(無論是正常結束還是異常情況),控制權將返回到直譯器,直譯器可能會找到其他需要編譯的跟蹤。

