計算機只能執行二進制代碼
也許你已經知道,計算機是基于二進制運行的。就像道家哲學的陰陽一樣,計算機只有兩個狀態,開或關、真或假、1或0…因為,組成計算機的基本元件——半導體只能以二進制進行計算。我們編程所用的C/C++、Python、大數據、AI等層出不窮的技術,以及我們存儲在電子設備的文本、音頻、圖像、視頻等媒介,最終都是以二進制的形式,被計算和處理的。計算機體系最底層的工程師要使用二進制代碼控制芯片來做計算和處理。
我在我的Mac上編寫了一個名為的程序,其二進制和匯編代碼如下所示:
首行的表示這是一個可以運行在64位x86架構的處理器上、基于Mac OS的一段程序。不同的計算機芯片廠商所設計的半導體電路不同,在芯片上編程的二進制規則不同。執行同樣的一段的邏輯,在基于ARM架構芯片的Android手機上所需要的二進制代碼與上面展示的會截然不同。當前市場上計算機CPU芯片基本被幾大科技公司壟斷,除了剛提到的Intel和AMD研發的應用在個人電腦上的x86-64處理器,應用在手機、平板電腦等移動設備上的ARM架構處理器,還有應用在大型服務器和超級計算機上的IBM Power系列處理器等。不同架構的CPU處理器都有自己的一套指令集(instruction set architecture,簡稱ISA),這就像一個設計圖紙和使用說明書,告訴編程人員如何使用在其芯片上進行編程:包括如何進行加減乘除計算,如何從內存中讀取數據等指令操作。底層開發人員會根據不同指令集,適配不同的CPU處理器。計算機能執行的指令,又被成為機器語言或機器碼。
前面所展示的二進制文件是一個。什么是可執行文件呢?可執行文件就是二進制機器語言的集合,可以被機器執行,得到我們想要的結果。我們在Windows上常會遇到的文件,就是可執行文件,其實是的縮寫,從手機應用商店下載的APP也是可執行文件的一種變體。
C語言從源代碼到可執行文件
很多朋友覺得C/C++編程調試難,沒有比較就沒有傷害,看到前文所提到的一個簡單加法的程序竟然需要這么多看不懂的01代碼,是不是覺得C語言簡直是天才般的發明。是的,C語言的發明者當時考慮的就是不同芯片廠商有不同的指令集,相互之間難以兼容,于是想在那些晦澀難懂的底層語言上,建立一個更為通用的編程范式,這樣編程人員不用浪費時間精力去識記大量的01二進制指令。那C語言代碼是如何轉化為可被機器執行的二進制文件呢?編譯器和操作系統是兩個非常關鍵的技術。
下面繼續以加法計算源代碼為例,展示編譯器和操作系統計算機將C語言轉化為機器可執行文件。
Linux和Mac OS用戶可以使用這個命令來將的源代碼編譯成名為的可執行文件,會生成在當前的文件夾下。
執行這個二進制文件,結果將被打印到屏幕上:
是一款開源的編譯器,是GNU Compiler Collection中的一員,它可以將C語言代碼編譯成可執行文件。GNU Compiler Collection還有C++編譯器、Fortran編譯器,并且支持包括x86-64和ARM在內的不同指令集。
C語言從源代碼到執行,要使用編譯器來編譯(compile)、匯編(assembly)并連接(link)所依賴的庫,形成機器可執行文件。執行這個二進制文件時,操作系統會為程序分配內存和CPU資源。“編譯”和“匯編”,相當于將C語言翻譯成底層語言。另外,代碼中使用了庫函數,當我們使用別人寫好的函數時,需要將這些前人寫好的庫函數連接到我們的可執行文件中,否則會調用函數失敗的錯誤。我們將這種需要編譯的語言稱為編譯型語言。編譯型語言有C/C++、Fortran等。
操作系統和編譯器是緊密相連的,不同操作系統所提供的編譯環境不同。Linux和GCC編譯器密不可分,Windows有自家研發的MSVC(Microsoft Visual C++)。不同操作系統在管理網絡、讀寫硬盤、圖形化等具體的實現方式不同,庫函數連接方式不同…可執行文件一般需要調用這些操作系統接口,所以最終連接生成的可執行文件會截然不同。了解了編譯知識,就不難明白為什么很多軟件提供商對同一個軟件會提供Windows、Mac OS、Linux、iOS、Android等多個版本的下載。因為不同平臺的硬件、編譯器和操作系統存在著巨大差異,可執行文件完全不同。所以,也就不難理解Windows軟件為什么不可能在Mac OS上運行。
實際構建一個大型項目時,編譯要考慮的問題會更多。比如我自己編寫了多個文件,文件1會被文件2調用,所以要先編譯文件1,后編譯文件2,否則會因為順序顛倒而報錯;還比如編譯型語言對所以依賴的庫函數非常挑剔,如果版本過低,有可能出現編譯錯誤。類似的問題會很多,因此編譯型語言在編程和調試時更麻煩,實際操作中一般會使用構建工具鏈(toolchain),根據一定的順序,從前到后串起來地去編譯。
解釋型語言:Java、Python、R…
既然可以將01組成的機器語言抽象成容易編寫的C語言,那為什么不能繼續再用類似的辦法,再做一次包裝呢?IT圈的一句名言就是:計算機科學任何領域的問題都可以通過增加一個中間層來解決。一些大牛忍受不了C語言這樣編寫和調試太慢,系統平臺之間無法共享移植的問題,于是開始自立門戶,創建了新的編程語言,最有名的要數Java和Python,這類語言不需要每次都編譯,因此被稱為解釋型語言。matlab、R、JavaScript也是解釋語言。
解釋型語言一般是使用C語言等偏底層的語言做一個或者,編程人員需要先在自己的計算機上安裝這個解釋器,接下來就只用關心自己的源代碼,其他的事情都交給解釋器去做。如果把編譯型語言的編譯過程比作將源代碼“翻譯”成機器語言的話,那么解釋型語言就是同聲傳譯。編譯型語言是一篇提前就“翻譯”好的稿子,拿過來就能被讀出來,這樣肯定更快;解釋型語言要等翻譯邊“聽”邊“翻譯”,速度當然慢很多。
不同編程語言的性能測試 - https://julialang.org/benchmarks/
C語言和相應編譯器經過了幾十年的發展,在性能優化上已經達到了極致,一般是所有高級語言中速度最快的。上圖展示了一個對不同編程語言在不同任務上的測試,數據以C語言為基準,可以看到Python、R等語言在部分任務上要比C語言慢10倍到100倍。Julia語言是解釋語言中的“奇葩”,它剛剛誕生沒幾年,語言的設計上使用了更多新技術,屬于長江后浪推前浪了。
有了解釋器,我們可以在任何安裝了Python的機器上運行同樣一份源代碼文件。像Python這樣的解釋語言就像一個高級計算器,非常容易上手,有一些理工基礎的朋友,半天時間就能學會。
其實,這就是一個妥協的過程,解釋語言放棄了速度,取得了易用性和可移植性。
如果我還是關心速度呢?當然還是要回歸底層,拒絕中間商賺差價嘛!
以Python為例,為了保證性能,大部分高性能科學計算庫其實都是使用編譯型語言編寫的。比如,感興趣的朋友可以前往numpy的源碼地址(https://github.com/numpy/numpy)查看,會發現很多C語言編寫的代碼。對于一些計算密集型的函數和方法,Python用戶自己可以使用這樣的工具,R語言可以使用。我最近在使用Java的jni來調用C++代碼,發現速度有成倍提升。
另一種方案是JIT(Just-In-Time)技術。JIT把需要加速的代碼編譯成了機器語言,不再需要“同聲傳譯”拖累自己了。我在Python上用庫進行過JIT測試,同樣的代碼會有8倍以上的速度提升。
我以后也會在我的專欄中介紹如何對解釋語言進行加速。