前言
大家好。歡迎來到十一月。希望你一切都好。
我們剛度過超級忙碌的十月。我們才剛推出一項對社群解答的重大改進。 我們現在會將它們去重,所以類似的解答只會顯示一次;我們加入了新的排序選項、依程式碼搜尋的功能,而如果你去看看 C#,就會發現我們還加入了依不同程式設計概念篩選的功能。 所以你可以搜尋使用位元位移、遞迴,或任何你感興趣技巧的解答。我們會在社群週把這項功能推展到其他學習軌道。
不過現在,讓我們把焦點放在 #12in23。 十月是個有趣的月份,探索了物件導向語言,但這個月我們要來硬的。 我們這個月聚焦在組合語言,具體來說是 MIPS Assembly、x86-64 Assembly 和 WebAssembly。 一如往常,Erik 會為我們解說這些語言有趣又獨特的地方。
徽章
一如往常,只要完成這些語言中任 5 個練習,就能獲得 Nibbly November 徽章。我們也有年度徽章,我知道你們很多人都在朝它努力。為此我們準備了 5 個精選練習供你完成。它們是:
- 位元計數:計算一個數字中 1 位元的數量
- 麥粒:計算在方格數量逐格倍增的棋盤上的麥粒總數
- 電阻色碼:將電阻色帶的顏色轉換為其數值表示
- 旋轉密碼:實作旋轉密碼(又稱凱薩密碼)
- 核苷酸計數:計算每個核苷酸在 DNA 字串中出現的次數
背景
為什麼叫 Nibble November?
嗯,你大概知道,一個 byte 是 8 個位元,而一個 nibble 是 4 個位元。如果把 byte 裡面的 y 換成 i,就會得到 bite,這麼一來這個名字就說得通了。nibble 就是小小的一口。
什麼是組合語言?
好,我們從一些基本概念開始吧。CPU 會執行指令,例如「將兩個數字相加」或「將位元左移」等等。這些指令稱為機器碼指令,它們不過是特定的位元序列。執行一支程式「不過是」(加引號)CPU 處理並執行這些位元序列而已。
由於直接把指令寫成一串串位元既麻煩又容易出錯,Kathleen 和 Andrew Donald Booth 早在 1947 年就想出一種對人類更友善的語言來表示機器碼指令。這種用來表示機器碼指令的語言就稱為組合語言。這種組合語言接著會透過「組譯器」轉換成機器碼指令。
組合語言一個有趣的地方是,它們與 CPU 架構直接綁定,不過通常與作業系統無關。
順帶一提,如果你記得打孔卡片,也就是最早期的電腦用來執行程式的那種大張卡片,它們也算是一種組合語言!
組合語言和我們現在寫程式的語言有什麼不同?
關鍵差異在於,組合語言非常低階。你會發現很多習以為常的抽象都不見了。類別或物件完全找不到。迴圈?你得靠跳躍指令自己寫。函式?想都別想!我發現寫組合語言程式碼讓人很謙卑,因為你會意識到現代語言替你省下多少工夫。不過,寫組合語言也極其有用,因為你會更了解事情實際上是怎麼運作的。
有趣的小知識:《RollerCoaster Tycoon》有 99% 的原始碼是手寫組合語言!這是個驚人的成就,等你自己寫過一些組合語言之後,會更加佩服。
現在還有人在寫組合語言嗎?
嗯,比以前少多了。以前手寫的組合語言常常能勝過編譯器產生的機器碼(這也是 C++ 這類語言允許直接嵌入組合語言的其中一個原因),但現在編譯器產生機器碼的功力太好了,這種情況已很少見。話雖如此,在效能至上或資源受限的環境裡,你還是會看到有人使用組合語言。
概覽
MIPS
- MIPS(Microprocessor without Interlocked Pipelined Stages,無內鎖管線階段的微處理器)是精簡指令集電腦(RISC)指令集架構的一個家族
- 由 MIPS Computer Systems 開發,於 1985 年首次發布
- 有多個版本:MIPS I、II、III、IV、V 以及 MIPS32/64。前兩個版本僅支援 32 位元,但 MIPS III 導入了 64 位元支援。
- 有數個選用的擴充,例如 SIMD 指令與壓縮
- 對後續的 RISC 架構影響深遠
- MIPS 在 2021 年宣布不再開發 MIPS 架構,並轉向 RISC-V(一個開源、免授權金的架構)
- 主要用於嵌入式系統(例如路由器)與伺服器(Silicon Graphics 的電腦曾採用它,這些電腦以電影特效聞名)、NEC Cenju-4 超級電腦、Tesla 的 Model S 汽車、NASA 的 New Horizons 探測器,但也用於大學教授組合語言,以及多款遊戲主機(例如初代 PlayStation、PlayStation Portable 和 Nintendo 64)
x86-64 Assembly
- 由 AMD 設計,於 1999 年以 AMD64 架構發布
- 它是 x86 指令集的 64 位元版本,而 x86 可追溯至 1978 年 Intel 推出 8086 微處理器。那是一款 16 位元處理器,但後來的 80386 加入了 32 位元指令,而那套指令集也成了 x86 的代名詞。
- 64 位元帶來的核心能力是存取更多記憶體(32 位元定址上限為 4GB),而這已成為瓶頸。64 位元理論上可定址 16 EB,但目前只用了 48 位元,可定址 256 TB(日後有需要時還能擴充)
- AMD64 擴充了 x86 指令集,並透過相容模式設計成與現有的 16 位元與 32 位元應用程式完全相容
- Intel 在 AMD 未參與的情況下設計了 IA-64。那是一套全新、非常不同且不向後相容的 64 位元指令集。最後 AMD64 勝出,Intel 也實作了自己的版本(只有一些細微的語意差異)
- 無所不在。從工作站到伺服器(包括超級電腦),從嵌入式系統到遊戲主機(例如 PS5 和 Xbox Series X)。
WebAssembly
- 由 W3C 設計,這是制定網頁技術標準的組織
- 設計目標是:
- 快速、安全且可攜
- 高效率且可攜的表示法
- 早年要在網頁上快速執行通常得靠特製的瀏覽器外掛,例如 Flash 和 Silverlight,因為 JavaScript 本身並不太適合高效能運算。這些外掛最大的缺點是通常有許多安全性問題,而且未經標準化。
- Mozilla 設計了 asm.js,這是 JavaScript 的一個子集,目標是讓程式碼能在瀏覽器中以絕佳的效能執行,它透過型別一致性(型別不會動態改變)與不使用垃圾回收來達成。各種語言便能編譯成 asm.js,在網頁上執行時仍保有良好效能。不過它終究還是 JS,能達成的效果有限。於是就有了新語言的提案:WASM。
- 它是一種類似組合語言的語言,提供一組要執行的指令。關鍵在於它不綁定特定 CPU,因此與平台無關,但每個平台都需要有對應的實作(虛擬機器)。這表示 WebAssembly 其實是位元組碼,而不是機器碼
- 靜態型別(與 JS 的關鍵差異)
- 通常採用提前編譯或即時編譯(但也可以解譯執行)
- 開放標準,定義了兩件事:
- 二進位格式
- 文字格式(會被編譯成二進位格式)
- 所有主要瀏覽器都有實作
- 用於許多需要高效能的網頁,例如 Google Earth、Figma、Unity 和 Autocad。在伺服器端也越來越受歡迎,例如用來執行微服務、在 SaaS 平台(例如 CloudFlare workers)或 Docker 中執行
從程式設計的角度來看,它們有何不同?
MIPS
- 採用載入/儲存架構(又稱暫存器對暫存器),指令不是做記憶體存取就是做算術運算,但都只對暫存器中的資料進行操作
x86-64 Assembly
- 採用暫存器與記憶體架構,允許對記憶體(或從記憶體)進行運算,也可對暫存器運算
WebAssembly
- 採用堆疊式程式設計(沒有暫存器),並可選擇從記憶體讀取資料或寫入記憶體
這些語言有什麼過人之處?
MIPS
- 精簡。MIPS 指令集的所有指令可以塞進一頁
- 既定的呼叫慣例有助於釐清如何使用可用的暫存器,例如哪些用來傳遞引數、哪些用來回傳結果。
- 穩定。最後一個版本於 2014 年發布
- 文件豐富,尤其在學術文獻中
- 實際應用廣泛。數十億台裝置
x86-64 Assembly
- 雖然是 x64 的擴充,但加入了許多新功能,包括:
- 64 位元整數支援
- 額外的暫存器
- SSE 指令(向量指令)
- 相對定址資料存取(使用共享函式庫時更有效率)
- 不可執行位元(防止在特定記憶體分頁中執行程式碼的安全功能)
- 熟悉。由於它擴充了 x86 指令集,對熟悉 x86 指令集的人來說相對容易學習 豐富且詳盡的文件
- 穩定。雖然新版本定期加入,但其核心極為穩定且向後相容
WebAssembly
- 相較於實體處理器(包括 RISC)的組合語言,堆疊式的 WebAssembly 虛擬機器精簡又單純。這讓它相對容易作為編譯目標。
- WebAssembly 文字格式使用 S 運算式「語法糖」來達成熟悉的命令式風格,再轉換成堆疊式程式碼。帶有 S 運算式的形式稱為「加糖形式」,它會「去糖」成另一種與二進位內容等價的形式。任何用過 LISP 的人對 S 運算式都會感到熟悉
- 與 JavaScript 有強大的互通性。在與 JavaScript 之間傳遞資料很簡單。重要但書:WASM(目前)還不允許與 DOM 互動
- 持續改進中。不只是 WASM 虛擬機器在改進,標準本身也在積極開發。大量新功能正在設計與開發中,例如 SIMD 相關指令、垃圾回收、執行緒、尾呼叫最佳化等等
- 安全。程式碼會在沙箱環境中驗證並執行,相較於 JavaScript 或原生組合語言提供了更高程度的靜態驗證。語意定義明確,易於檢查與推理
突出特色
MIPS
- 效率。MIPS 處理器非常有效率,很適合嵌入式系統。
- 效能。效能優異,因此 MIPS 曾被用於超級電腦
- 易學。指令少,而且每道指令只做一件簡單的事,因此容易學習。非常適合教學。
x86-64 Assembly
- 強大。x86-64 經過數十年的調整,擁有大量有助於效能提升的指令。其中一個例子是 SIMD(Single Instruction, Multiple Data,單指令多資料),這類指令可以……嗯,讓一道指令同時對多筆資料平行執行。
- 無所不在。執行 x86-64 的裝置到處都是。Intel 和 AMD 的 CPU 都有實作。它長久以來一直是實質標準
- 定期更新。例如新的向量指令,有 SSE3-5、AVX、AVX-512 等等
WebAssembly
- 高效率。二進位格式精簡,能用快速的單次遍歷解碼、驗證並編譯。它也可以串流處理,讓解碼、驗證與編譯能在看完所有資料之前就盡早開始。而且它還能平行處理。這使它非常適合高效能的網頁應用程式。
- 絕佳的編譯目標。讓許多語言的程式碼都能在網頁上執行。大多數主要語言都支援編譯成 WebAssembly 二進位檔,這表示你的程式碼不必寫 JavaScript 就能在網頁上執行。有些語言不是把你的程式碼編譯成 WebAssembly,而是把執行環境編譯成 WebAssembly,再由它執行未經修改的位元組碼。
- 易於部署與執行。你只需要一個能解譯位元組碼的虛擬機器,而所有主要瀏覽器都內建了一個。
- 不綁定網頁,也能在伺服器端執行。WebAssembly System Interface(WASI)是一套設計成可攜至任何平台的介面(ABI 與 API)。它類似 POSIX(Unix 系統的標準介面),提供 I/O 之類的功能。安全性是其設計的核心,包括沙箱化以及以能力為導向(檔案或 socket 之類的權限都必須明確要求)。WASI 也有潛力讓不同語言之間輕鬆互通。Docker 共同創辦人 Solomon Hykes 在 2019 年寫道:「如果 WASM+WASI 在 2008 年就存在,我們根本不需要打造 Docker。」
該選哪一個
- 如果你從未接觸過組合語言,WebAssembly 大概是最好上手的語言。話雖如此,如果你想學一種會編譯成機器碼的組合語言,就試試 MIPS Assembly 吧
- 如果你用的是 x86-64 機器(很有可能),試試 x86-64 Assembly 吧
- 如果你熟悉 LISP,你會很欣賞 WebAssembly 採用 S 運算式這一點
- 如果你在做網頁應用程式,WebAssembly 是最合理的選擇
- 如果你在意效能,x86-64 和 MIPS 都是很棒的選擇。或者如果你在意網頁效能,就試試 WebAssembly 吧