程式語言:程式是如何運作的?
1. 前言
這堂課將介紹各種關於程式語言的知識理論,並用各位熟悉的 Ruby 語言來舉例。
要學會開車,不需要知道汽車內部構造原理,但是如果要當職業賽車手,就需要瞭解汽車內部各個元件的作用了,瞭解這些零件是如何整合在一起的。這樣才可以用得更好,避免誤用。萬一半途拋錨了,也能很快做故障排除,知道哪個零件壞了會有什麽影響,可不可以繼續開,如果要修又要多少時間和成本。不過,你仍然不需要知道太底層的東西,例如怎麽真的去製作內燃機、輪胎等零件。
在完成這堂課之後,你可以回答以下問題:
- 程式在電腦中是如何執行的?
- Ruby 程式語言和其他程式語言有什麽不同?
- 電腦的架構是什麽?記憶體是幹嘛的?
- 作業系統是幹嘛的?
- 各種數據型態的差異
- 為什麽有亂碼? 什麽是 Unicode 萬國碼?
- 什麽是正規表示法 Regular Expression,這可以幹嘛?
- 什麽是算法、資料結構?
- 如何用 BigO 評估算法效能,瞭解不同資料結構的效能差異
2. 什麽是程式語言
程式語言用來描述電腦如何工作,電腦很厲害可以處理超多資料、每秒可以做數億次的操作(operations),但是同時也很笨,每個操作很簡單機械、沒有見解或理解。程式語言也需要很精確,需要固定語法(syntax)結構讓電腦可以瞭解。
世界上很有很多程式語言,為什麽有這麽多種語言呢?讓我們先分成兩種:低階語言和高階語言來說明。
低階語言
低階語言指的是機器語言和組合語言,一步一步指示電腦微處理器如何動作,是最原始的程式語言。低階語言不是說比高階語言差,而是指抽象化的程度比較低,與電腦硬體的指令直接對應的意思。
首先,一臺電腦的組成,包括中央處理器(CPU)、記憶體(RAM)、硬盤和其他 I/O 設備(網卡、鍵盤、鼠標和螢幕等)。其中記憶體就像是短期記憶,所有正要執行的指令和數據,都會先放在記憶體上。CPU 會從記憶體讀取出要執行的指令和數據,執行完的結果再放回記憶體。記憶體的速度很快,但是只要關機重開,數據就會不見,而且容量較小。為了持久化數據,我們用硬盤來存成檔案,來做持續保存。硬盤的空間很大,也比較便宜。
中央處理器(CPU)有自己的指令集,不同廠商的 CPU 指令集不一樣,例如 Intel x86 和 ARM 不一樣。Intel 的 CPU 主宰了桌機和伺服器市場,特色是效能高跑得快。而 ARM 架構的 CPU 特色是省電,因此大部分的手機和行動裝置,都是用 ARM 架構的 CPU。
機器語言會指示 CPU 進行什麽操作:首先將指令和數據從記憶體搬進 CPU 的寄存器,接著 CPU 進行計算,然後將結果從CPU 寄存器搬回記憶體,代碼長得像這樣:
000000 00001 00010 00110 00000 100000
100011 00011 01000 00000 00001 000100
000010 00000 00000 00000 10000 000000
看不懂意義是正常的。其中每一行內容就是某個 CPU 指令、CPU 寄存器編號,以及記憶體位址。這個記憶體位址的長度是固定的,目前主流是 32 位元或 64 位元 (所以 CPU 跟作業系統,還有分 64 位元和 32 位元,指的就是這個長度)。
電腦的數字信號是以二進制數來表示的,一個 0 或 1 就是一個位元(bits)。8個 bits 稱為 1 bytes 是常見電腦計算容量的基本單位,32位元也就是4bytes的長度。在往上則是 1000 byte 或 1024 bytes 為 1 kilobyte (1KB)。1000 或 1024 KB 是 1 Megabyte (1 MB)、1000 或 1024 MB 是 1 Gibibyte、1000 或 1000 GiB 是 1 Tebibyte (1TB)。什麽時候用十進制 1000 什麽時候用二進制的 1024 (二的十次方)呢?對商人有利的時候就會用十進制,例如賣你硬盤容量的時候講 1T,但是硬盤格式化之後,電腦是用二進制運作的,所以變成 930 GB 而已。
機器語言人類是很難直接閱讀和撰寫,因此 CPU 廠商發明了組合語言,基本上就是對應機器語言,只是給予符號意義,長得像這樣:
MOV eax, 1
ADD eax, 4
SUB eax, 2
MOV num, eax
INVOKE printf, ADDR formatStr, num
ret 0
低階語言寫起來很費事,但電腦跑起來飛快。註意,不同 CPU 的機器碼(又叫做 native code)是不一樣的。因此 Intel x86 和 ARM CPU 的代碼、32位元和64位元的程式是不通用的。
這世界上 10 種人,懂二進制的,以及不懂二進制的。
高階語言
由於撰寫低階語言的開發速度太慢,因此我們會用所謂的高階語言來開發軟體。高階語言透過結構化的程式設計語法,包括變數、數據型態 函式、控制結構、循環等等功能,來讓開發好讀好寫。
這些高階語言例如 C 語言、Java 語言、PHP/Python/Ruby 等等。其中最重要的發明就是 C 語言了,長得像這樣:
#include <stdio.h>
int main() {
int n, i, sum = 0;
printf("Enter a positive integer: ");
scanf("%d",&n);
for(i=1; i <= n; ++i) {
sum += i; // sum = sum+i;
}
printf("Sum = %d",sum);
return 0;
}
請存成 sum.c
,然後執行 cc sum.c -o sum
就會編譯出 sum
這個執行檔,用 ./sum
就會執行。這個程式會從 1 累加到你輸入的一個數字。
在 C 語言中,使用變數需要先宣告數據型態,例如這裡是 int
表示整數。它會預先跟記憶體要固定的空間(int 會要 4 bytes)。在 C 語言中,要使用變數都必須跟記憶體先講要多少空間。
電腦可以執行機器碼,但是無法直接消化高階語言,這些高階語言的源代碼都必須經過一個編譯(compile)的過程,轉換成二進制機器語言,也就是可執行的檔案。
這個編譯的工具,叫做編譯器(Compiler)。你不必弄明白代碼是怎麽變成二進制碼的機器語言,但你得知道這個過程。
電腦軟體可以分成系統軟體和應用軟體,前者包括作業系統、編譯器、嵌入式系統等等,不希望有任何效能損耗,需要瞭解硬體、操控硬體,例如記憶體空間。後者則是各種 App、桌面軟體、手機軟體、Web 應用等,效能上可以有權衡(trade-off),好寫好改 vs. 程式執行效能,來因應變來變去的商務需求。
其中 C 語言是最重要的系統程式語言,目前絕大部分程式語言的編譯器,都是用 C 語言寫的,例如 Ruby 也是用 C 語言寫的。C 源碼經過編譯後可以移植到不同硬體上(例如 Intel 或 ARM,同一份 C 源碼,用不同平臺編譯器編譯出不同的機器碼)上,執行的效能非常好。
3. 什麽是作業系統
關於程式語言是如何運作的,我們還漏介紹一個關鍵的系統軟體,那就是作業系統。
一臺電腦不只跑一隻程式,而是同時有非常多程式在執行,另外還有各式各樣不同廠商的I/O設備。而作業系統就是負責管理這些硬體資源的程式,它會負責管理如何分配記憶體給不同的程式和優先級、控制I/O設備,例如硬盤、網卡、鼠標、鍵盤等等,並且提供一個用戶接口讓我們可以安裝和操作不同應用軟體。
因此,上一章有提到的高階語言需要透過編譯器轉換成機器碼,會因為不同 CPU 架構而不同,也會因為不同的作業系統而不同。不同的作業系統,會提供不同的 API 讓程式語言可以調用硬體資源。程式語言不需要知道不同廠商的硬盤怎麽調用,程式語言內部會調用作業系統所提供的檔案 API,而不同廠商的硬盤,會提供驅動(Driver)軟體與作業系統串接。
作業系統例如微軟的 Windows、Apple 的 MacOS、Linux 等等。
讓我們動手操作一下,觀察一下作業系統的運作。請打開 Mac 的 Activity Monitor
其中每隻正在執行的程式,就叫做 Process 進程,會有一個編號是 PID。每個 Process 會被作業系統分配一整塊記憶體,以及分配給一個 CPU 執行,不同 Process 可以同時執行。如果有多個 CPU 就是真的平行處理,如果只有一顆 CPU,那作業系統也會依照優先級依序讓不同 Process 執行,因為 CPU 很快就好像是平行執行的一樣。
如果一個 Process 內想要平行處理,那可以再生出輕量的 Thread(線程),不同線程可以分配到不同 CPU 上執行,但是是共享同一塊記憶體。撰寫多線程的程式是困難的,我們不太會接觸。
4. 記憶體管理
瞭解記憶體是編程非常重要的概念,因為如果你把記憶體用光了,作業系統就得去把硬盤模擬成記憶體使用,但是由於硬盤的速度跟記憶體差太多了,整台電腦的效能會急劇下滑,就會呈現當機的狀態。一臺電腦的記憶體是有限的,你的 MacBook 可能只有 4G 或 8G,租一臺伺服器,最重要的也是先看有多少記憶體空間可以使用。記憶體越多,可以同時執行的程式就越多。
打開 Mac 的 Activity Monitor,點擊 Memory 可以觀察各個程式使用記憶體的情況
前幾節提到在 C 語言中,使用變數需要預先跟記憶體索要空間,這跟 Ruby 很不一樣。在 Ruby 是全自動的記憶體管理,你學到現在好像都不需要關心到底記憶體是如何被使用的吧。但是在 C 語言中需要手動管理記憶體。在硬體資源有限或需要效能至上的軟體中,手動管理記憶體有其必要,但是缺點就是降低了開發效率,開發者必須注意好記憶體管理,用的時候要先宣告,不用的時候要釋放回作業系統。如果一隻程式一直消耗記憶體,卻從來不歸還給作業系統,那這只程式就是不斷不斷地耗用,直到全部記憶體都被吃光,最後電腦就當機了(或是手機上的作業系統會強制關閉你的應用)。
以程式語言的發展歷史來看,第二重要的可能就是 Java 語言了。Java 有許多重大的發明,其中物件導向我們下一個教程會教的重點,另外就是內部 Virtual Machine(VM)跨平臺設計,以及和垃圾回收 Garbage Collection (GC) 了,我們先談談 GC。
GC 是程式語言的一種內部功能,作用是自動把再沒有用到的變數,把記憶體釋出回作業系統。例如你可以想像,程式語言在執行的時候,會定時停一下檢查所有變數,看看哪些變數已經沒有被使用,就釋放回作業系統。
垃圾回收可以讓程式員減輕許多負擔,也減少程式員犯錯的機會。在 Java, Ruby, JavaScript 都有 GC,一般來說不需要特別煩惱記憶體的使用。還是有可能碰到記憶體泄露(memory leak)問題,例如不斷使用記憶體但沒有釋放的機會。
例如以下 Ruby 程式,是一個無窮循環不斷增加資料到陣列裡面:
arr = []
i=0
while(true)
arr[i] = "hahaha!"
i = i+1
end
請繼續觀察 Mac 的 Activity Monitor,等等你會發現有一隻 Ruby 程式,它的記憶體用量不斷上升……… (請記得中斷這個程式)。
程式語言內部的 GC 算法非常重要,非常影響程式語言的執行效能。GC 多久執行一次,每次執行 GC 要花多少時間(一跑 GC,你的程式就等於是暫停下來),這些都會嚴重影響程式的進行。例如在一些 real-time 強即時性的軟體中,就說是一個機器人走路的軟體好了,是不能用 GC 的,需要手動管理記憶體。想像一下走路腿抬到一半,然後程式語言不定就自動跑了 GC 暫停一下,那就跌倒了。
近十年 Ruby 1.9 到 Ruby 2.4 的版本,都著重在改進內部的 GC 算法,以來增加效能。
5. 編譯型語言和解釋型語言
高階語言又可以分為兩種:編譯型語言(靜態語言/Static)和解釋型語言(動態語言/Dynamic),這兩種語言的優缺點,一直以來都是開發者社群最愛論戰的話題。
編譯型語言包括:C 語言、C++、Java 語言等等,這種語言要求一定要先把全部代碼編譯變成機器碼(native code),也就是可執行的檔案。軟體散佈和分享的時候,是拿最後的執行檔。在這類語言中,使用變數必須事先宣告類型,例如這個變數一開始宣告是 int,那就一定只能是 int,不能換成存字串。
解釋型語言包括: Ruby、Python、JavaScript、PHP 等等,這種語言不需要先編譯,而是透過一種叫做解釋器(interpreter)的軟體,逐行編譯然後直接執行。軟體散佈和分享的時候,是拿源代碼。在這類語言中,使用變數不需要事先宣告類型,一開始存整數,後來換成存字串也可以。
編譯型語言每次修改代碼,都必須重新把程式編譯好,如果程式任一行有錯,就無法編譯。但是因為都先編譯好了,所以執行的速度比較快,而且執行檔很小。剛剛的 C 語言範例,編譯後只有8K的大小。
解釋型語言不需要先編譯,修改起來比較方便,寫代碼也快一些,但是任何錯誤都要等到真正執行之後,才會知道。因為要等到執行時才編譯,所以執行的速度較慢。一個只有一行的 Ruby 代碼程式,如果要再另一臺電腦跑起來,那台電腦也必須把 Ruby 解釋器安裝起來,而且跑起來至少需要 5MB 的記憶體,即使只是輸出一行 Hello World。
所以為何有不同的程式語言呢?一方面是大家對於效率的需求不一樣,有的希望是執行快,有的希望開發快。一般來說 「機器語言 -> 組合語言 -> C 語言 -> Java 語言 -> 動態語言」越往右邊跑起來效能較差,但開發起來效率比較好。
另一方面也是程式語言的設計哲學不同,有些喜歡功能多、程式碼比較有表現力,例如 Scala,但語言本身會比較複雜。有些喜歡功能少,比較精實但打比較多字,例如 Go 語言。
為什麽 Web 應用,使用解釋型語言有更好的的優勢?
著名的”人月神話”一書作者Fred Brooks曾說:「一個程式設計師一天能產生的程式碼行數是差不多的,無論什麽程式語言」。因此一個具有表達能力的高階程式語言,就會比低階的程式語言能完成更多功能。相較於靜態程式語言,使用更高階的動態腳本語言可以幫助我們:
- 用更少程式碼做更多事情,大大增加生產力
- 更快因應客戶開發需求,敏捷開發
不過,動態語言也不是沒有缺點:
- 執行效能是絕對比不上靜態語言的
- 沒有編譯期可以檢查型別錯誤
但是,我們知道現在的電腦越來越快、越來越便宜、上網越來越容易、記憶體越來越多、硬盤越來越大。另外,行動裝置也越來越多,需要搭配的網路服務需求也增加了。這些趨勢告訴我們有更多的軟體的需求,另一方面由於硬體效能的增強,人力開發成本比起軟體的執行期的效能,也越來越重要。同樣一個程式,用動態語言執行的效能已經可以達到實用(例如每秒可以處理50~500個的HTTP請求,也可以透過增加伺服器來擴展架構),也許用靜態語言後的執行速度可以再快一倍,但是卻需要十倍以上的時間來開發,這件事情是不是值得呢?
在硬體資源有限的行動裝置及嵌入式系統上,仍是靜態語言的天下,這一點需要更多時間才有動態語言的生存空間。 沒有編譯期可以檢查型別錯誤的問題,也隨著單元測試和TDD(Test-driven development)測試驅動開發等敏捷最佳實務而逐漸降低重要性。而大部分的Bug會出自於商業邏輯錯誤,而不是型別錯誤上。
6. 各種程式語言介紹
討論各種程式語言的優劣,也是程序員經常筆戰的熱門話題。以下簡單介紹一些常見的程式語言:
- C 語言: 開發系統程式 (System Programming)、作業系統、編譯器等等工具必備語言。經過編譯可以移植到不同硬體上。
- C++ 語言: 多了一大堆功能和麵向物件的超級複雜版 C 語言,大型軟體如 Google Chrome, Qt, WebKit, V8, HHVM 或是效能要求高的游戲等等,用 C++ 比較多。但是 C++ 由於語法太多太複雜,被認為是最不好上手的語言之一。Linux 和 Git 的發明人 Linus,就堅決反對 C++。
- Java 語言: 提供跨平臺 VM、物件導向,使用 GC 記憶體垃圾自動回收,由於發展已久,效能也非常好,後端很多企業軟體和中間件使用,在超大型網站中也十分常見,例如阿裡巴巴、Twitter、Linkedin 服務端。前端方面要 Android 軟體也是用 Java 語言,
這裡提一下什麽是跨平臺,這有一些歧義。Java 當年的一個理念是跨平臺(Windows、Mac、Linux 等):同一份編譯好的執行檔,可以在不同平臺執行,不需要像 C 語言需要編譯成不同執行檔。為了達成這麽目的,Java 發明了一種叫做字節碼(bytecode)的設計,Java 會先編譯成這種與平臺沒有依賴的字節碼,但是每個平臺需要先裝 Java VM (JVM),又叫做 JRE(Java Runtime Environment),現在新的作業系統應該都有內建了。
這麽說 Ruby 其實也是跨平臺,因為同一份 Ruby 源碼也可以跑在 Windows、Mac、Linux 上,只要 Ruby 的解釋器(interpreter)能裝上去就行。不過通常沒這麽幸運,因為很多 Ruby 庫是用 C 語言寫的,而不是 Ruby,因此不一定能在不同平臺上順利編譯成功,因為這些 C 代碼可能有依賴作業系統的,例如調用了某個只在 Linux 作業系統上才有的 C 庫。因此很多需要編譯的 gem 例如 nokogiri 甚至都必須針對 Windows 提供不同的 gem 版本。
不過說到真正的跨平臺,目前的主流是 Web 應用,瀏覽器才是真正跨平臺的軟體。用戶不需要預先安裝 VM 也不需安裝動態語言的解釋器,只需要有個瀏覽器就可以了。
- Scala 語言、Clojure 語言、JRuby 語言等等:這些語言都建構上 JVM 上,透過編譯變成 Java 字節碼,就可以在不同平臺上執行。這些語言用自己偏好的語法設計,然後搭上 Java VM 發展成熟的便車,可以調用 Java 的庫。
- C# 語言: 微軟的官方語言,當年是仿 Java 所推出的程式語言。它的 .NET framework 等同於 JVM 的設計。在 .NET 上還有其他微軟的程式語言 VB.NET, ASP.NET, F# 等等
- Objective-C 和 Swift,蘋果專用的程式語言,撰寫 MacOS 和 iOS 應用必備
- PHP 語言,當年發明的時候叫做 Personal Home Page,所以叫做 PHP。PHP 的初衷是作為 HTML 樣板(就像是 Rails 裡面的 html.erb),主攻 Web 應用。因為容易上手使用,在 2000 年初搭配 MySQL 資料庫非常流行。
- Python 語言: 也是動態語言的一種,和 Ruby 時常拿來對比。近年來在數據分析和機器學習領域用得很多。
- JavaScript 語言: 託瀏覽器的福,成為全世界最風行的語言。後端可以用 Node.js 單獨將 JavaScript 跑在伺服器上,而不需要依賴瀏覽器的環境。
- R 語言: 用於數據分析領域,學術領域用的很多。但不會拿來做軟體應用。
別人用什麽?
- Java: Google, Oracle
- Swift, Objective-C: Apple
- C#: Microsoft, stack overflow
- PHP: wikipedia, vimeo, facebook
- Ruby: airbnb, shopify, github, twitter, groupon, basecamp, hulu+
- Python: youtube, quora, google, instagram, pinterest
程式語言有許多概念和功能是跨語言都有的,只是 ecosystem (衍生出來的套件、社群和支援)不一樣,擅長的情境不一樣,一般來說:
- 開發系統程式(例如作業系統、編譯器),適合 C 語言
- 開發 Web 後端應用,適合 PHP/Ruby/Python/Node.js
- 開發 Web 前端應用,得用 JavaScript
- 開發 Android 應用,得用 Java
- 開發 iOS 應用,得用 Swift 或 Objective-C
最後,「程式語言」和「程式語言的實作」是不一樣的概念,前者是指語法的規格定義,後者是指編譯器(或解釋器)。同一門程式語言,但是有不同家的編譯器(或解釋器)是常見的事情,例如:
- JavaScript 的語法標準叫做 ECMAScript,但是 JavaScript 的實作有很多,包括 Chrome 瀏覽器用的 V8 引擎、Safari 用 WebKit、Firefox 用 SpiderMonkey。雖然都叫 JavaScript 語言,但是真正跑在不同瀏覽器時,實際上是不同的解釋器,還是有差異的。
- Ruby 有 CRuby(又叫做 MRI,大家目前安裝的就是 MRI 版本)、JRuby、Rubunius、 RubyMotion 等等
- PHP 有 Zend Engine (這是官方版) 和 HipHop (這是 Facebook 針對 PHP 重寫過的 PHP 解釋器,以改進效能)
- Objective-C 和 Swift 當然就謹此 Apple 一家出編譯器
- .NET 有微軟官方版和 Mono
- Java 有 HotSpot (這是 Oracle 的官方版本) 和 OpenJDK (這是開源版本,在 Linux 安裝的話會裝到這個版本)
參考資料
- Standford University: Computer Science 101
- 代碼之髓:程式語言核心概念
- 松本行弘的程式世界:成為一流程式設計師的14種思考術
- 演算法的樂趣, 基峰
- Introduction to Programming with Ruby
- Object Oriented Programming with Ruby
- Seven Languages in Seven Weeks
- Ruby 物件導向設計實踐