在进一步学习如何设计处理器之前, 需要先了解处理器工作的基本原理
本次课内容:
了解这些概念后, 就可以
电子计算器通过按键控制, 可以进行四则运算
1+2=
CPU的功能远比电子计算器强大
指令
通过加法指令控制CPU对两个数据相加
->
指令需要给出两方面信息:
操作数
(operand)字段
源操作数
和目的操作数
操作码
(opcode)字段类似生活中的菜单: 番茄炒鸡蛋, 回锅肉, 清蒸鲈鱼…
一些复杂的处理过程需要分步进行, 依次用不同的指令处理数据
1+2+...+10
, 需要先计算1+2
,
再将结果与3
相加…
需要临时存放指令处理的中间结果 - 寄存器
寄存器有多个, 称为通用寄存器(General Purpose Register, GPR)组
指令需要在操作数字段中指定
指令 = 用二进制编码的菜单
某简单处理器有4个GPR, 支持3种指令, 其中2种如下:
7 6 5 4 3 2 1 0
+----+----+-----+-----+
| 00 | rd | rs1 | rs2 | R[rd]=R[rs1]+R[rs2] add指令, 寄存器相加
+----+----+-----+-----+
| 10 | rd | imm | R[rd]=imm li指令, 装入立即数, 高位补0
+----+----+-----+-----+
R[rs1]
表示编号为rs1
的GPR中的内容li
指令的源操作数是立即数
:
直接将指令中的imm
字段解析成一个二进制数在计算机看来, 程序=指令序列
计算机跟计算器的最大不同: 让程序来自动控制计算机的执行
既然PC存储了当前执行指令的位置, 那PC也应该是一个寄存器
bner0
指令, Branch if Not Equal r0
7 6 5 2 1 0
+----+---- -----+-----+
| 11 | addr | rs2 | if (R[0]!=R[rs2]) PC=addr bner0指令, 若不等于R[0]则跳转
+----+----------+-----+
0: li r0, 10 # 这里是十进制的10
1: li r1, 0
2: li r2, 0
3: li r3, 1
4: add r1, r1, r3
5: add r2, r2, r1
6: bner0 r1, 4
7: bner0 r3, 7
用(PC, r0, r1, r2, r3)
的格式来记录CPU状态的变化过程:
PC r0 r1 r2 r3
(7, 2, 3, 1, 8) 表示接下来将要执行编号为7的指令, 当前4个GPR的值分别为2, 3, 1, 8
约定在开始的时刻, CPU的状态是(0, 0, 0, 0, 0)
---------------------------------------------------------------------------
(0, 0, 0, 0, 0) # 初始状态
(1, 10, 0, 0, 0) # 执行PC为0的指令后, r0更新为10, PC更新为下一条指令的位置
(2, 10, 0, 0, 0) # 执行PC为1的指令后, r1更新为0, PC更新为下一条指令的位置
(3, 10, 0, 0, 0) # 执行PC为2的指令后, r2更新为0, PC更新为下一条指令的位置
(4, 10, 0, 0, 1) # 执行PC为3的指令后, r3更新为1, PC更新为下一条指令的位置
(5, 10, 1, 0, 1) # 执行PC为4的指令后, r1更新为r1+r3, PC更新为下一条指令的位置
(6, 10, 1, 1, 1) # 执行PC为5的指令后, r2更新为r2+r1, PC更新为下一条指令的位置
(4, 10, 1, 1, 1) # 执行PC为6的指令后, 因r1不等于r0, 故PC更新为4
(5, 10, 2, 1, 1) # 执行PC为4的指令后, r1更新为r1+r3, PC更新为下一条指令的位置
......
1+2+...+10
只需要不到40条指令,
CPU只需花费0.00000002s, 即20ns
1+2+...+10000
li r0, 10000
(需要更长的指令来表示10000
这个立即数)
通过指令的组合实现需求, 也是编程!
上述add
和li
指令, 属于汇编语言:
指令的符号化表示
还有更底层的机器语言: 指令的二进制表示, 可以被数字电路实现的CPU直接执行
10001010 # 0: li r0, 10
10010000 # 1: li r1, 0
10100000 # 2: li r2, 0
10110001 # 3: li r3, 1
00010111 # 4: add r1, r1, r3
00101001 # 5: add r2, r2, r1
11010001 # 6: bner0 r1, 4
11011111 # 7: bner0 r3, 7
根据约定的编码规则, 汇编语言和机器语言可以互相转换
GPR, PC, 存储器, 指令及其执行过程,
在计算机领域中属于指令集架构
(Instruction Set Architecture,
缩写为ISA, 也简称指令集)的范畴
ISA的本质是一系列规范(通常记录在手册中), 定义了一台模型机的功能和行为
状态集合\(S = \{(PC, R, M)\} = \{(PC, r0, r1, r2, r3, M)\}\)
\[\begin{array}{l} & S_k=(5, 10, 1, 0, 1, [10001010, 10010000, 10100000, \\ & 10110001, 00010111, 00101001, 11010001, 11011111]) \end{array}\]
状态转移规则就是以下3条指令的语义:
7 6 5 4 3 2 1 0
+----+----+-----+-----+
| 00 | rd | rs1 | rs2 | R[rd]=R[rs1]+R[rs2] add指令, 寄存器相加
+----+----+-----+-----+
| 10 | rd | imm | R[rd]=imm li指令, 装入立即数, 高位补0
+----+----+-----+-----+
| 11 | addr | rs2 | if (R[0]!=R[rs2]) PC=addr bner0指令, 若不等于R[0]则跳转
+----+----------+-----+
00101001
后的次态R[2] = R[2] + R[1]
, 故\(S_{k+1}=(6, 10, 1, 1, 1)\)
初始状态\(S_0 = (0, 0, 0, 0, 0)\)
x86, ARM, RISC-V这些商业级别的真实ISA
除了指令的语义之外, ISA还包括:
汇编语言也可以编程, 但如果要开发更大的程序, 汇编语言并不方便
一个数列求和程序: 计算1+2+...+10
# r0, r1, r2和r3分别指代4个GPR, 并且用逗号来分隔指令的操作数
# 冒号前的数字表示PC, 井号及其后的文字表示注释
0: li r0, 10 # 这里是十进制的10
1: li r1, 0
2: li r2, 0
3: li r3, 1
4: add r1, r1, r3
5: add r2, r2, r1
6: bner0 r1, 4
7: bner0 r3, 7
现代的程序开发通常采用高级编程语言
/* 计算`1+2`并输出结果 */
/* 1 */ int main() {
/* 2 */ int x = 1;
/* 3 */ int y = 2;
/* 4 */ int z = x + y;
/* 5 */ printf("z = %d\n", z);
/* 6 */ return 0;
/* 7 */ }
一些说明:
/*
和*/
之间的内容是C语言的注释(comment)
int
表示整数类型, 它是integer
的缩写int main() { ... }
定义了一个名称为main
的函数
main
函数是一个特殊的函数, 它是C程序的入口
main
函数开始执行/* 计算`1+2`并输出结果 */
/* 1 */ int main() {
/* 2 */ int x = 1;
/* 3 */ int y = 2;
/* 4 */ int z = x + y;
/* 5 */ printf("z = %d\n", z);
/* 6 */ return 0;
/* 7 */ }
{ ... }
是函数体, 由语句组成,
每条语句用于指示程序执行一个操作
int x = 1;
定义了一个变量, 变量的名称为x
,
并将其初值赋为1
;
结束
int y = 2;
同理int z = x + y;
定义了一个变量z
,
并将其初值赋为x + y
/* 计算`1+2`并输出结果 */
/* 1 */ int main() {
/* 2 */ int x = 1;
/* 3 */ int y = 2;
/* 4 */ int z = x + y;
/* 5 */ printf("z = %d\n", z);
/* 6 */ return 0;
/* 7 */ }
printf("z = %d\n", z);
是一个函数调用语句,
它调用了函数printf
"z = %d\n"
和z
代入printf
的定义中并执行
printf
是一个特殊的函数,
用于向终端输出信息%d
表示将第二个参数z
按十进制格式输出z
的当前值按十进制格式输出到终端/* 计算`1+2`并输出结果 */
/* 1 */ int main() {
/* 2 */ int x = 1;
/* 3 */ int y = 2;
/* 4 */ int z = x + y;
/* 5 */ printf("z = %d\n", z);
/* 6 */ return 0;
/* 7 */ }
return 0;
是函数返回语句
0
作为计算结果返回给调用该函数的语句
C语言的组成
直觉: 变量\(\approx\)GPR, 语句\(\approx\)指令
/* 1 */ int main() {
/* 2 */ int x = 1;
/* 3 */ int y = 2;
/* 4 */ int z = x + y;
/* 5 */ printf("z = %d\n", z);
/* 6 */ return 0;
/* 7 */ }
/* 1 */ int main() {
/* 2 */ int sum = 0;
/* 3 */ int i = 1;
/* 4 */ do { /* 一个do-while循环, 先执行循环体 */
/* 5 */ sum = sum + i;
/* 6 */ i = i + 1;
/* 7 */ } while (i <= 10); /* 再判断循环条件: 成立时重复执行循环体; 否则继续执行后续语句 */
/* 8 */ printf("sum = %d\n", sum);
/* 9 */ return 0;
/* 10*/ }
/* 1 */ int main() {
/* 2 */ int sum = 0;
/* 3 */ int i = 1;
/* 4 */ do { /* 一个do-while循环, 先执行循环体 */
/* 5 */ sum = sum + i;
/* 6 */ i = i + 1;
/* 7 */ } while (i <= 10); /* 再判断循环条件: 成立时重复执行循环体; 否则继续执行后续语句 */
/* 8 */ printf("sum = %d\n", sum);
/* 9 */ return 0;
/* 10*/ }
A B C D
S0 = <0, 0, 0, 0>
S1 = <1, 0, 0, 0>
S2 = <1, 1, 0, 0>
S3 = <1, 1, 1, 0>
S4 = <1, 1, 1, 1>
S5 = <0, 1, 1, 1>
S6 = <0, 0, 1, 1>
S7 = <0, 0, 0, 1>
S8 = <0, 0, 0, 0> = S0
从状态机的视角来说, CPU和这个计数器并没有本质上的区别
从状态机视角理解C程序的执行过程 = 在我们的思维中执行这个C程序
但计算机只能执行指令, 它无法理解C程序的含义
sum = sum + i;
的代码
怎么让计算机执行C程序?
/* 1 */ int main() {
/* 2 */ int sum = 0;
/* 3 */ int i = 1;
/* 4 */ do { /* 一个do-while循环, 先执行循环体 */
/* 5 */ sum = sum + i;
/* 6 */ i = i + 1;
/* 7 */ } while (i <= 10); /* 再判断循环条件: 成立时重复执行循环体; 否则继续执行后续语句 */
/* 8 */ printf("sum = %d\n", sum);
/* 9 */ return 0;
/* 10*/ }
C程序 | ISA | |
---|---|---|
状态 | \(\{PC, V\}\) | \(\{PC, R, M\}\) |
激励事件 | 执行语句 | 执行指令 |
状态转移规则 | 语句的语义 | 指令的语义 |
编译器的工作: 将C程序的状态机翻译成ISA的状态机
构造 \[\begin{array}{l} compile_S:\{PC, V\}\to\{PC, R, M\}\\ compile_E:\{语句\}\to\{指令序列\} \end{array}\] 使得 \[\begin{array}{l} compile_S(next(S_{C,k}, 语句)) \\ =next(compile_S(S_{C,k}), compile_E(语句)) \\ \end{array}\]
从而有 \[\begin{array}{rl} &compile_S(S_{C,k+1}) \\ =&compile_S(next(S_{C,k}, 语句)) \\ =&next(compile_S(S_{C,k}), compile_E(语句)) \\ =&next(S_{ISA,k},指令序列) \\ =&S_{ISA,k+1} \\ \end{array}\]
其中, \(S_{C,k}\)表示C语言状态机的第\(k\)个状态, \(S_{ISA,k}\)表示ISA状态机的第\(k\)个状态
说人话: C程序执行一条语句后的状态, 与ISA模型机执行编译得到的指令序列后的状态, 语义上是等价的
C程序 | ISA | |
---|---|---|
状态 | \(\{PC, V\}\) | \(\{PC, R, M\}\) |
激励事件 | 执行语句 | 执行指令 |
状态转移规则 | 语句的语义 | 指令的语义 |
具体地, 编译器需要完成以下工作:
ISA | 数字电路 | |
---|---|---|
状态 | \(\{PC, R, M\}\) | 时序逻辑电路 |
激励事件 | 执行指令 | 处理组合逻辑 |
状态转移规则 | 指令的语义 | 组合逻辑电路的逻辑 |
CPU设计的工作: 用数字电路的状态机实现ISA的状态机
构造 \[\begin{array}{l} CPUdesign_S:\{PC, R, M\}\to\{时序逻辑电路\}\\ CPUdesign_E:\{指令\}\to\{组合逻辑电路\} \end{array}\] 使得 \[\begin{array}{l} CPUdesign_S(next(S_{ISA,k}, 指令)) \\ =next(CPUdeisgn_S(S_{ISA,k}), CPUdesign_E(指令)) \\ \end{array}\]
从而有 \[\begin{array}{rl} &CPUdesign_S(S_{ISA,k+1}) \\ =&CPUdesign_S(next(S_{ISA,k}, 指令)) \\ =&next(CPUdesign_S(S_{ISA,k}), CPUdesign_E(指令)) \\ =&next(S_{CPU,k},组合逻辑电路) \\ =&S_{CPU,k+1} \\ \end{array}\]
其中, \(S_{CPU,k}\)表示CPU电路状态机的第\(k\)个状态
说人话: ISA模型机执行一条和指令后的状态, 与设计出的CPU在组合逻辑电路控制下的次态, 语义上是等价的
ISA | 数字电路 | |
---|---|---|
状态 | \(\{PC, R, M\}\) | 时序逻辑电路 |
激励事件 | 执行指令 | 处理组合逻辑 |
状态转移规则 | 指令的语义 | 组合逻辑电路的逻辑 |
具体地, CPU设计需要完成以下工作:
程序和指令集都没有实体, 计算机的实体是电路, 如何联系它们?
状态机模型是理解复杂系统的一种有效方法
不过最重要的是给大家传达一种观念
机器永远是对的
计算机系统的行为是按照官方手册的描述精确发生的
永远
的理解: 商业产品也会有bug(Intel奔腾的fdiv
bug), 但你恰好遇上的概率很小
用户在计算机中感受到的一切, 都是通过程序执行一条条指令来实现的!
为了实现更高效的计算过程, 现代ISA提供更多指令:
计算机架构师还致力于设计出能更高效执行指令的计算机:
让计算机用户获得越来越快的性能体验
RISC-V: 标准和实现分离, 企业竞争的是如何设计出更好的处理器
包云岗老师对文章《点评RISC-V芯片出货量突破100亿》的补充评论:
出自《从技术的角度来看,RISC-V 能对芯片发展、科技自主起到哪些作用?》
推荐参加一生一芯补充基础知识, 零基础亦可学习
这个问题本质上是对基于开放标准的治理机制
理解不到位
C程序 | ISA | CPU | |
---|---|---|---|
状态 | \(\{(PC, V)\}\) | \(\{(PC, R, M)\}\) | \(\{时序逻辑电路\}\) |
状态转移规则 | C语言语句的语义 | 指令的语义 | 组合逻辑电路 |
FM | C语言标准手册 | 指令集手册 | 架构设计文档 |