【譯】Ruby2.6的JIT功能,編譯和解釋型語言的相關說明

lanzhiheng發表於2019-02-03

一篇闡述編譯以及解釋相關概念的文章,對於理解計算機語言的編譯以及解釋的過程有一定的好處,同時還會了解到JIT到底是個什麼東西。原文連結: medium.com/@mich_berr/…


Ruby2.6

Ruby2.6在幾個星期之前釋出了,新特性是一個嶄新的Just-In-Time(JIT)編譯器. 你可以點選這裡讀到關於關於新特性的更多細節,但如果你在尋找更多的背景知識,那麼我想一個闡述編譯型語言,解釋型語言,以及JIT是如何工作的相關指引會對你有所幫助。

首先,什麼是程式語言?所有的程式語言都是機器程式碼的抽象。機器程式碼是由位(許多的0和1)組成的,可以通過你機器的硬體進行儲存和執行。讓人類去閱讀和編寫二進位制是非常低效的事情,這也是我們發明程式語言的理由。

解釋和編譯的不同之處主要在於人類編寫的程式是怎麼轉換成機器可以執行的指令集的。有的時候程式語言並不能明確地劃分為是到底是編譯型的還是解釋型的;編譯和解釋是把你寫的程式碼轉換成機器可讀程式碼的兩種不同的策略。我們會在討論JIT的時候瞭解到編譯和解釋之間的界線變得越發模糊。

編譯

簡單來說,編譯就是把高階的程式語言轉換成機器能夠識別的語言。

讓我們拿C語言來做個例子,它是一門有代表性的編譯型語言。為了執行C語言寫的程式,必須使用像gcc或者clang這樣的編譯器,把C語言的原始碼編譯成能適應你計算機的機器碼。需要重點指出,不同的電腦可能會有不同的CPU架構,意味著一臺電腦處理0和1序列的方式跟另一臺電腦是不一樣。一個編譯器會把原始碼轉換成特定架構的機器程式碼。一旦你的程式碼被編譯完,它可以按你希望的那樣執行在任何具有相同架構的系統上。不過如果你更新了原始碼,或者想要把你的程式執行在具有不同架構的機器上的時候,你就需要對程式碼進行重新編譯了。

編譯器的一個很好的類比就是人類的翻譯人員。跟翻譯人員把西班牙語的書翻譯成英文書籍那樣,一個編譯器就是把人類編寫的原始碼翻譯成機器程式碼。一旦一本書被翻譯完,任何懂英語的讀者都能夠閱讀它。然而如果原來西班牙語版本的書籍有所改動,英文版將需要重新翻譯相關的部分並再次釋出。

解釋

不像編譯器可以在程式執行之前預先把原始碼翻譯成機器碼,直譯器是一行接著一行,一邊翻譯一邊執行。繼續之前的類比,計算機直譯器就像一個口譯人員。他們充當西班牙語以及英語談話者交流的橋樑,一句句實時地翻譯出來。

我們用Ruby作為“解釋型”語言的一個例子。Ruby1.8以及更早的版本,那個時候的Ruby直譯器(MRI), 它的行為就像上面描述的那樣。它讀取每一行Ruby程式碼,解析並token化,然後使用一個樹型的資料結構來執行它。而從版本1.9開始,Ruby切換到一個包含YARV(Yet Another Ruby Virtual Machine)的實現。在這個實現裡,Ruby會被預編譯成位元組碼,這樣命名是因為它們會佔用一個位元組的記憶體空間。一個非常簡單的例子,2+3 將會把加法運算轉換成位元組碼的形式,並且接收23作為引數,一旦Ruby程式碼被轉換成位元組碼,這些位元組碼將會由虛擬機器一行一行地執行。把原始碼轉換成位元組碼對提升執行速度有重大意義。

Python當然也會利用位元組碼,你可以直接在Python程式產生的.pyc檔案裡面看到它。這些檔案的作用就像是一個快取;如果Python程式碼再次執行,卻沒有作任何修改,可以跳過編譯的步驟直接執行相關的位元組碼檔案。當Ruby程式設計成位元組碼之後,它的位元組碼只是儲存在記憶體中而不是持久化到檔案。

Just-In-Time編譯

我剛剛描述了一些當代的一些解釋型語言如今是怎麼包含位元組碼編譯這一步驟的,但還有另一種方式,它會使編譯與解釋之間的界線開始模糊起來,被稱為Just-In-Time編譯。

最流行的Just-In-Time編譯器就是Java虛擬機器(JVM)了。Java是一門靜態型別的程式語言,它可以直接被編譯成機器程式碼,但是這通常都會通過JVM來完成轉換。在近期的案例中,Java程式碼會被Javac編譯器編譯成Java位元組碼,這些位元組碼會由JVM來翻譯並執行。然而JVM並不會一句句地去翻譯位元組碼。相反,在這個時候它會嘗試去組合像函式那樣的有意義的程式碼塊。而要決定怎麼去組合這些程式碼塊將會稍微有點耗時,不過最終會使執行更加高效。這個過程被稱之為Just-In-Time編譯,因為它的行為就像是一個編譯器,區別就是它會在執行時進行編譯。使用JVM的好處是它既保留了編譯型語言的效能特徵又讓Java可以像解釋型語言那樣移植到不同的機器上。Java是世界上最流行的程式語言,很大一部分原因要歸功於它的JVM。

除了JVM之外,已經有其他的VM專案採用了JIT編譯。PyPy是一個包含JIT功能的Python直譯器,當然現在的Ruby也有可選的JIT功能了。

折衷

現在你已經對編譯型以及解釋型語言有個大概的瞭解了,那麼他們各自是如何權衡的呢?

速度

編譯型語言通常會比解釋型語言快許多。一個編譯好的C程式可能要比Python,Ruby這樣的解釋型語言快上好幾個量級。然而Java的JIT解決方案也是非常高效的,它的執行速度可以幾乎可以媲美C語言所寫的程式。

可移植性

為了在不同架構的機器上執行你編譯好的程式,你需要重新編譯它。當一門語言被解釋之後,它的指令集可以在具有不同架構的機器上執行(當然要有相關的虛擬機器)。JVM就是很好的例子,它結合瞭解釋以及編譯兩門技術,最大程度地兼顧了速度以及可移植性。

動態型別 VS 靜態型別

編譯器必須要把應用程式轉換並組合成機器指令,這是非常死板的。當宣告一個變數的時候,編譯器需要明確地知道是什麼型別的變數以及需要為它分配多少記憶體空間。這就是為什麼編譯通常都要求靜態型別。作為對比,直譯器一行行地執行相關的程式,因此他們的行為更靈活。

在Ruby裡面,我們可以寫出像2 + 3 或者"a" + "b"這樣的程式碼,直譯器會在執行時確定物件的型別,不管是整數的相加,還是字串的拼接,都會為它們呼叫正確的方法。

Bugs/除錯

解釋型語言通常更容易除錯,因為程式在遇上錯誤之前會一直執行。直譯器將會告知使用者具體是哪一行引發的執行時錯誤,反之,在編譯好的程式裡面bug比較難以發現。

FAQS

為什麼把一門語言看成是“編譯型語言”或者“解釋型語言”是用詞不當的?

一門語言是根據他們的語法以及資料結構來確定的。編譯及解釋是把語言的語法轉換成可以在硬體上執行的形式的兩種不同的實現方式。“編譯”或者“解釋”並不是語言的天性;同一門語言可能會同時包含這兩種實現方式。

為什麼人們把Python看作是解釋型語言把C看作是編譯型語言?

他們都是參考了該語言最通用的實現或者是發行版本。然而Python也能夠被編譯,C語言當然也可以被解釋。

什麼是虛擬機器?

一個虛擬機器就是一切行為像計算機那樣的抽象,意味著它能夠接收一系列的指令,與硬體的實現方式不同,它是通過軟體來實現的。虛擬機器通常用於在計算機的一個作業系統上執行其他的作業系統。舉個例子,你有一個windows的筆記本,但是想要模擬一個Linux的作業系統。你會利用一個軟體,它隱蔽在你物理機的作業系統跟你想要執行的作業系統之間。這被稱為一個“系統虛擬機器”。

不要跟之前的例子混淆,那些是“程式虛擬機器”。比如,Java的虛擬機器(JVM)或者Ruby的虛擬機器(YARV)。這些都會被看作是虛擬機器,因為它們可以接收指令集並執行相關的位元組碼。虛擬機器的好處是對硬體進行抽象並提供了一個平臺獨立的程式設計環境。

為什麼人們通常認為Python和Ruby有直譯器,而Java有虛擬機器?直譯器是否也符合虛擬機器的定義?

直譯器和虛擬機器的更多是語義上的區別,或者像這個人所闡述的是“社會構建”上的區別。我覺得直譯器是符合虛擬機器的定義的。嚴格意義上,Ruby和Python的直譯器都包含能夠處理相關位元組碼的虛擬機器。而另一方面,JVM本質上跟Ruby或者Python的直譯器有所不同。我已經在這篇文章中觸及了一小部分的原因,不過剩下的知識已超出這篇文章的範圍。

為什麼解釋一門語言之前先把它編譯成位元組碼能使速度加快?

位元組碼會比原始碼耗費更少的記憶體空間,並且更容易被直譯器執行。當一門語言要被編譯成位元組碼,直譯器必須要解析所有的語句,把他們轉換成位元組碼,然後解析位元組碼並執行它們。對一個簡單的程式碼片段而言,這個中間的步驟會小幅度地增加了執行的時間。然而,對那些需要重複執行的程式碼,比如,迴圈或者是可複用的方法等,位元組碼的步驟能夠大幅度提升速度。

更多資源:

BaseCS,一個關於程式設計基礎概念的很不錯的部落格https://medium.com/basecs/a-deeper-inspection-into-compilation-and-interpretation-d98952ebc842

Ruby原理剖析,深度談論Ruby內部機制的書籍,書中有一些C語言程式碼,程式設計師應該會感到親切:patshaughnessy.net/ruby-under-…

Bradfield Academy上的計算機組成方面的課程,關於計算機的組成概念的優秀課程:bradfieldcs.com/

Tyler Elliot Bettilyon的youtube視訊,Tyler是我在Bradfield上的指導員,是星球上把CS概念闡述得最好的人類之一https://www.youtube.com/watch?v=KsZLPTRSleI

相關文章