ISA 是軟體、CPU之間的規則:軟體可以用哪些機器指令,硬體用這些指令要產出什麼結果。
ISA (Instruction Set Architecture)
我自己平常比較喜歡白話一點的方式,
所以這邊列幾點舉例,
基本上,ISA 就是給 CPU 看的語言規則:
-
能不能做加法?加法指令怎麼寫、怎麼 encode?
-
有哪些 registers 可以用?每個有多少 bytes?
-
如何從 memory 讀資料、寫回資料?
-
怎麼跳去其他位置執行程式?
常見的 ISA 有:x86-64、AArch64、RISC-V
Code Explanation
我們用 C 語言來舉例
#include <stdint.h>
uint32_t add(uint32_t a, uint32_t b){
return a + b;
}
把兩個 unsigned 32 bit int 相加回傳
在執行過程 compiler 會轉成目標 ISA 的 assembly
x86-64
Intel 語法、System V AMD64 Calling Convention:
add:
mov eax, edi; edi 先丟進 eax,而且變數傳遞順序是 di->si->dx
add eax, esi; eax 是我們的回傳值,所以加上去就好,不用再另外找 register
ret
AArch64
Apple Silicon 使用的指令集
add:
add, w0, w0, w1; w0 = w0 + w1
ret
w0, w1是 32 bit,對應到 x0, x1 是 64 bit
RISC-V
以 RV32I 為例
add:
add a0, a0, a1 # a0 = a0 + a1
ret
a0, a1 是 x10, x11 的慣用別名
做點區分
看到這邊可能也會開始有人把 ISA, Calling Convention 開始搞混了,
這邊我們比較一下
| 項目 | 說明 |
|---|---|
| ISA | 有哪些暫存器、指令,指令如何運作 |
| Calling Convention,屬於 ABI 的一部份 | 參數放哪個暫存器、回傳值放哪、誰負責保存暫存器 |
記憶體的例子
既然談到 C 語言,
那我們也勢必要來看看 pointer 的例子:
#include <stdint.h>
void increase(uint32_t *p){
*p = *p + 5;
}
關於 uint32_t 的部分可以參考 < 我還沒打完,之後更新XD >
假設,
uint32_t value = 10;
increase(&value); // 做完後 value = 10
用 RV32I assembly 表示:
increase:
lw t0, 0(a0) ; 0(a0) 是指 register a0 的 addr 再加上 offset 0
addi t0, t0, 5 ; t0 = 10 + 5 = 15
sw t0, 0(a0) ; 把 t0 的 32-bit 資料寫回原地址,所以 value = 15
ret
像這個例子我們就可以看出 RISC-V 一個很重要的規則,
-> Load-Store Architecture
- 一般整數算術指令:用 register 操作處理
- 要處理記憶體中的數值:先用 load 讀,再用 store 寫回
這個理解後我們看 x86-64 應該會比較容易懂,
同樣的東西會被表示成:
increase:
add DWORD PTR [rdi], 5 ; [rdi] 代表 rdi 存的地址指向的記憶體
ret ; p.s. DWORD 代表大小是 32-bit
一行就可以直接解決,
但要特別注意,
這邊比較少行不代表效能比較快,
一條指令不代表一個 clock 週期,
也不會保證比三條指令快,
CPU 還是要走完 read, compute, write,
相信看到這邊應該有會有人問了,
Q: 「阿那個 add 不就是一條 assembly,怎麼會有需要拆開來看的問題?」
這邊我們可以再探討多一點:
Dive Inside
其實像 add 這幾個字,跑的時候不會被 CPU 讀到,
他只是提供給人閱讀的 mnemonic(直翻是記憶術,但這邊可以看成是 容易理解的符號),
畢竟真正執行的還是 binary machine code,
再看一次 RV32I 的例子:
addi x5, x0, 5
意思是
x5 = x0 + 5
RISC-V 的 x0 固定讀出 0,所以是把 5 丟進 x5
這條指令的 32-bit machine code 是:
0x00500293
這串號碼其實就是 ISA 定義出來的
包括 32-bit 裡面:
- 哪些表示操作種類
- target register
- source register
- 常數 5
CPU 按照規則去 decode 並執行
btw 其實我們前面的 ret 在 RISC-V 也是 pseudoinstruction,
assembler 會把他轉成真正的指令:
jalr x0, 0(x1)
代表跳到 x1 儲存的 ret addr
這邊也放上 RISC-V 常見的 alias(ABI 定義):
| 編號名稱 | 別名 | 約定用途 |
|---|---|---|
x0 | zero | 固定讀出 0 |
x1 | ra | 儲存函式 ret addr |
x2 | sp | Stack pointer |
x5 | t0 | 暫存運算資料 |
x10 | a0 | 傳遞第一個參數,也用於回傳值 |
x11 | a1 | 傳遞第二個參數,也可用於回傳值 |
補充:ISA vs. microarchitecture
最後 自己有個鑽牛角尖的壞毛病 XD
雖然只是名詞上定義的問題
但其實一開始看我有花了一段時間分清楚他們的差別
所以還是整理了一個表格 給大家參考:
| 概念 | 在乎的東西 | 例子 |
|---|---|---|
| ISA | 軟體能要求 CPU 做什麼?結果必須是什麼? | add 如何運算、reg 有多寬 |
| microarchitecture | CPU 內部怎麼實現這些要求? | Pipeline、亂序執行、分支預測、Cache 配置 |
例如兩顆 CPU 都支援某一組 RISC-V 指令,
就必須讓這些指令都符合 ISA,
但一顆可以是 In-order execution CPU,另一顆走 OoO,
效能跟耗電上也可能就會差很多了
這個就是 microarchitecture 的範圍,
至於 in-order, out-of-order 是什麼,
我們就等之後的文章再看吧。