引言

在进一步学习如何设计处理器之前, 需要先了解处理器工作的基本原理

 

本次课内容:

  • 从数学模型的角度阐释一些和计算机相关的重要概念
    • 处理器, 指令集, 程序
    • 以及三者之间的联系

 

了解这些概念后, 就可以

  • 通过真正的数字电路实现处理器
  • 在这个处理器上运行程序

CPU的基本组成和工作原理

指令的概念

电子计算器通过按键控制, 可以进行四则运算

  • 依次输入按键1+2=

 

CPU的功能远比电子计算器强大

  • 处理方式肯定不止一种, 需要受某种方式的控制
  • 控制的媒介就是指令
    • 例如, 通过加法指令控制CPU对两个数据相加

 

通过加法指令控制CPU对两个数据相加 -> 指令需要给出两方面信息:

  1. 指定需要处理哪些数据: 指令的操作数(operand)字段
    • 源操作数目的操作数
  2. 指定用何种方式处理数据: 指令的操作码(opcode)字段

类似生活中的菜单: 番茄炒鸡蛋, 回锅肉, 清蒸鲈鱼…

寄存器 - 存放数据的部件

一些复杂的处理过程需要分步进行, 依次用不同的指令处理数据

  • 例如, 要计算1+2+...+10, 需要先计算1+2, 再将结果与3相加…

 

需要临时存放指令处理的中间结果 - 寄存器

  • 回锅肉需要先煮后炒, 煮好的肉要先盛到碗中

 

寄存器有多个, 称为通用寄存器(General Purpose Register, GPR)组

  • 厨房有碗柜, 里面有很多碗

 

指令需要在操作数字段中指定

  • 从哪个GPR中读出数据, 对应源操作数
  • 将计算结果存入哪个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
+----+----+-----+-----+
  • 指令只有3种, 操作码最少2位
  • GPR有4个, 用2位可以指定1个GPR的编号
    • R[rs1]表示编号为rs1的GPR中的内容
  • li指令的源操作数是立即数: 直接将指令中的imm字段解析成一个二进制数
一些指令编码的例子:
00100001   add指令, 将R[0]和R[1]相加, 结果写入R[2]
10110000   li指令, 将立即数0000写入R[3]
10000101   li指令, 将立即数0101写入R[0]

计算机的划时代思想: 存储程序

在计算机看来, 程序=指令序列

计算机跟计算器的最大不同: 让程序来自动控制计算机的执行

  • 先把一段指令序列放在存储器(如现代计算机的内存)中, 让计算机从内存中取出指令来执行
  • 当计算机执行完一条指令之后, 就继续执行下一条指令
    • 为了能让计算机知道下一条指令在哪里, 还需要有一个用于指示当前执行到哪条指令的部件: 程序计数器(Program Counter, PC)

 

计算机的工作:

重复以下步骤:
  从PC指示的存储器位置取出指令
  执行指令
  更新PC

将程序放在存储器中, 让PC指向第一条指令, 计算机就会自动执行程序

  • 如今各种主流计算机, 本质上都是存储程序计算机

可以修改PC的指令: 分支指令

既然PC存储了当前执行指令的位置, 那PC也应该是一个寄存器

  • 可以设计相应指令来修改PC(不属于GPR), 增加程序执行的灵活性
    • 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]则跳转
+----+----------+-----+

 

一个数列求和程序: 计算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

用大脑执行指令

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更新为下一条指令的位置
......

CPU = 不断执行指令的部件

  • 计算机的愚钝: 按照指令的含义机械地更新寄存器的状态
  • 计算机的聪明: 运算速度极快
    • 以2GHz的CPU为例, 1s进行2000000000次操作
    • 上述程序计算1+2+...+10只需要不到40条指令, CPU只需花费0.00000002s, 即20ns
    • 人工按计算器, 需要22次按键, 即使1s按10次, 也需要2s

 

  • 计算1+2+...+10000
    • 只需要将前文PC为0的指令改为li r0, 10000(需要更长的指令来表示10000这个立即数)
      • 执行约30000条指令, 花费15us
    • 人工按计算器, 需要48894次按键, 约4889s!
    • 即使用等差数列求和公式, 也需要几秒钟才能算出答案

重新审视编程

通过指令的组合实现需求, 也是编程!

 

上述addli指令, 属于汇编语言: 指令的符号化表示

还有更底层的机器语言: 指令的二进制表示, 可以被数字电路实现的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, 也简称指令集)的范畴

  • x86, ARM, RISC-V等概念, 其实都是ISA
  • 前文提到的只有3条指令的例子, 也是一种ISA
    • 将它称为sISA(simple ISA)

 

ISA的本质是一系列规范(通常记录在手册中), 定义了一台模型机的功能和行为

  • 模型机 = 只存在于思维中的机器, 只关注其功能和行为, 不讨论具体实现的细节
  • 用数字电路实现模型机的功能 -> 得到一台真正的计算机

状态机的(不严谨)定义

  • 状态集合\(S = \{S_1, S_2, \dots\}\)
  • 激励事件\(E\)
  • 状态转移规则\(next: S \times E \to S\)
    • 描述每个状态在不同激励事件下的次态(next state)
    • 二元函数\(next(S, E)\)给出了在状态\(S\)下接收到激励事件\(E\)后的次态
  • 初始状态\(S_0 \in S\)

ISA的状态机模型

  • 状态集合\(S = \{(PC, R, M)\}\)
    • \(R\) = GPR
    • \(M\) = 内存
  • 激励事件\(E = \{指令\}\)
    • 执行PC指向的指令, 改变状态
  • 状态转移规则\(next: S \times E \to S\)
    • 指令的语义(semantics)
    • 约定了执行某指令后, 状态应该发生怎么样的变化
  • 初始状态\(S_0 = (PC_0, R_0, M_0)\)

以sISA为例

状态集合\(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}\]

  • \([]\)表示内存中存储的信息, 此处为前文指令序列的编码
  • 由于在sISA中, 指令不会修改内存中存储的信息, 可以省略状态中对内存的描述: \(S_k=(5, 10, 1, 0, 1)\)

以sISA为例(2)

状态转移规则就是以下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]则跳转
+----+----------+-----+

 

  • \(S_{k+1}=next(S_k, 00101001)\)表示, 在状态为\(S_k=(5, 10, 1, 0, 1)\)时执行指令00101001后的次态
  • 指令行为是R[2] = R[2] + R[1], 故\(S_{k+1}=(6, 10, 1, 1, 1)\)

 

初始状态\(S_0 = (0, 0, 0, 0, 0)\)

更真实的ISA

  • 计算机的工作过程就是一个数学游戏
    • 状态机 = 游戏规则
  • 理解了状态机, 就已经做好准备理解计算机!

 

x86, ARM, RISC-V这些商业级别的真实ISA

  • GPR数量更多, 指令数量更多, 指令行为也更复杂
  • 但核心的本质都是状态机
    • 从抽象到具体: RTFM阅读相应手册

 

除了指令的语义之外, ISA还包括:

  • 输入输出, 系统状态, 中断异常, 虚存管理, 内存模型
  • 随着学习进度的深入会依次介绍

C语言入门

汇编语言的局限性

汇编语言也可以编程, 但如果要开发更大的程序, 汇编语言并不方便

  • 汇编语言编程需要关注数据如何在GPR之间流动
  • 而且一条指令能进行的操作普遍很少
    • 即使是一段简单的程序逻辑, 可能都需要十几条指令才能表达

一个数列求和程序: 计算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

现代的程序开发通常采用高级编程语言

  • 我们选择C语言, 容易建立程序与计算机系统工作过程的关联

一个简单的C语言示例

/* 计算`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 */ }

一些说明:

  1. /**/之间的内容是C语言的注释(comment)
    • 注释内容不影响程序的逻辑, 因此注释主要是给程序员阅读的
  2. int表示整数类型, 它是integer的缩写
  3. int main() { ... }定义了一个名称为main的函数
    • 函数是C语言的基本模块
    • 执行C程序的过程, 就是执行C程序中函数代码的过程
    • 特别地, main函数是一个特殊的函数, 它是C程序的入口
      • 也即, C程序将从main函数开始执行

一个简单的C语言示例(2)

/* 计算`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 */ }
  1. 大括号中的内容{ ... }是函数体, 由语句组成, 每条语句用于指示程序执行一个操作
    • 大括号中的语句默认按顺序执行
  2. int x = 1;定义了一个变量, 变量的名称为x, 并将其初值赋为1
    • 变量是程序中存储信息的对象
    • 在C语言中, 一条语句以分号;结束
      • int y = 2;同理
  3. int z = x + y;定义了一个变量z, 并将其初值赋为x + y

一个简单的C语言示例(3)

/* 计算`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 */ }
  1. printf("z = %d\n", z);是一个函数调用语句, 它调用了函数printf
    • 类似数学中的函数
      • 已知\(f(x)=x+1\), 求\(f(3)\), 就是将\(3\)代入\(f\)的定义中计算的过程
    • 上述语句将两个参数"z = %d\n"z代入printf的定义中并执行
      • 在C程序中, printf是一个特殊的函数, 用于向终端输出信息
      • 第一个参数中的%d表示将第二个参数z按十进制格式输出
    • 上述函数调用会将变量z的当前值按十进制格式输出到终端

一个简单的C语言示例(4)

/* 计算`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 */ }
  1. return 0;是函数返回语句
    • 表示该函数执行结束, 并将0作为计算结果返回给调用该函数的语句

 

C语言的组成

  • 变量 - 处理的对象
  • 语句 - 处理的操作流程
  • 输入输出函数 - 让变量与外界交互

C程序的状态机模型

直觉: 变量\(\approx\)GPR, 语句\(\approx\)指令

  • 状态集合\(S = \{(PC, V)\}\)
    • \(V = \{v_1, v_2, v_3, \dots\}\) = 程序中所有变量的取值
    • \(PC\) = 程序计数器 = 当前执行的语句位置
      • 大括号中的语句默认按顺序执行 -> 暗示有\(PC\)
  • 激励事件\(E = \{语句\}\)
    • 执行PC指向的语句, 改变程序的状态
  • 状态转移规则\(next: S \times E \to S\)
    • 语句的语义(semantics)
    • 约定了执行某语句后, 状态应该发生怎么样的变化
  • 初始状态\(S_0 = (main函数的第一条语句, V_0)\)

用状态机模型理解C程序的执行过程

/* 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 */ }

 

PC  x  y  z
(2, ?, ?, ?)    # 初始状态, ?表示变量未初始化或未赋初值
(3, 1, ?, ?)    # 执行PC为2的语句后, x更新为1, PC更新为下一条语句的位置
(4, 1, 2, ?)    # 执行PC为3的语句后, y更新为2, PC更新为下一条语句的位置
(5, 1, 2, 3)    # 执行PC为4的语句后, z更新为x + y, PC更新为下一条语句的位置
(6, 1, 2, 3)    # 执行PC为5的语句后, 终端输出"z = 3", PC更新为下一条语句的位置
(END, 1, 2, 3)  # 执行PC为6的语句后, 从main函数返回, 程序执行结束

用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语言编程至少有如下优势:

  • 变量的命名可以更直观地反映出其用途
    • 汇编语言中, GPR的用途只能根据上下文推断
  • 循环的表达更清晰, 可直接区别循环条件和循环体
    • 在汇编语言中, 循环条件和循环体都是指令, 需要根据上下文推断
0: li r0, 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

用状态机模型理解C程序的执行过程(2)

/* 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*/ }

 

PC sum i
(2, ?, ?)    # 初始状态
(3, 0, ?)    # 执行PC为2的语句后, sum更新为0, PC更新为下一条语句的位置
(5, 0, 1)    # 执行PC为3的语句后, i更新为1, PC更新为下一条语句的位置(第4行无有效操作, 跳过)
(6, 1, 1)    # 执行PC为5的语句后, sum更新为sum + i, PC更新为下一条语句的位置
(7, 1, 2)    # 执行PC为6的语句后, i更新为i + 1, PC更新为下一条语句的位置
(5, 1, 2)    # 执行PC为7的语句后, 由于循环条件i <= 10成立, 因此重新进入循环体
......

数字电路的状态机模型

数字逻辑电路 = 状态机

  • 数字逻辑电路 = 组合逻辑电路 + 时序逻辑电路

 

  • 状态集合\(S = \{(时序逻辑元件的值)\}\)
    • 具体包括寄存器, 存储器, 触发器等
  • 激励事件\(E = \{组合逻辑\}\)
    • 组合逻辑电路输出的信号驱动时序逻辑元件改变状态
  • 状态转移规则\(next: S \times E \to S\)
    • 由设计中的组合逻辑电路决定
  • 初始状态\(S_0 = (复位时时序逻辑元件的值)\)

例: Johnson计数器

      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程序

但计算机只能执行指令, 它无法理解C程序的含义

  • 因此也无法执行类似sum = sum + i;的代码

 

怎么让计算机执行C程序?

  1. 将C程序的代码翻译成行为等价的指令序列
  2. 然后让计算机的电路来执行这个指令序列

将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*/ }

 

这个过程称为编译(compile)

  • 原则上可以人工进行
  • 现代程序规模大, 人工编译非常繁琐
    • 通常由一类称为编译器(compiler)的特殊程序来开展编译工作
0: li r0, 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

编译 = 将C程序翻译成指令序列

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}\]

编译 = 将C程序翻译成指令序列(2)

从而有 \[\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程序翻译成指令序列(3)

C程序 ISA
状态 \(\{PC, V\}\) \(\{PC, R, M\}\)
激励事件 执行语句 执行指令
状态转移规则 语句的语义 指令的语义

 

具体地, 编译器需要完成以下工作:

  • 将C程序的状态翻译成ISA的状态, 也即
    • 将C程序的PC对应到ISA的PC
    • 将C程序的变量对应到ISA的GPR或内存
  • 将C程序的状态转移规则翻译成ISA的状态转移规则
    • 也即, 将语句翻译成指令序列

CPU设计 = 根据ISA设计数字电路

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}\]

CPU设计 = 根据ISA设计数字电路(2)

从而有 \[\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在组合逻辑电路控制下的次态, 语义上是等价的

CPU设计 = 根据ISA设计数字电路(3)

ISA 数字电路
状态 \(\{PC, R, M\}\) 时序逻辑电路
激励事件 执行指令 处理组合逻辑
状态转移规则 指令的语义 组合逻辑电路的逻辑

 

具体地, CPU设计需要完成以下工作:

  • 用数字电路的状态实现ISA的状态
    • 也即, 用时序逻辑电路实现PC, GPR和内存
  • 用数字电路的状态转移规则实现ISA的状态转移规则
    • 也即, 用组合逻辑电路实现指令的功能

程序如何在计算机上运行

程序和指令集都没有实体, 计算机的实体是电路, 如何联系它们?

 

  1. 根据ISA手册的功能描述, 画一张CPU的结构图 -> 处理器架构设计
  2. 根据结构图设计具体的电路 -> 逻辑设计
  3. 开发程序 -> 软件编程
  4. 将程序翻译成ISA手册中描述的指令序列 -> 编译
  5. 在CPU上执行程序 = 用程序编译出的指令序列控制CPU电路进行状态转移
    • 此时, 三个状态机产生联系: \(S_C\sim S_{ISA}\sim S_{CPU}\)

一句话就能说清楚, 为什么搞那么复杂?

状态机模型是理解复杂系统的一种有效方法

一句话就能说清楚, 为什么搞那么复杂?(2)

不过最重要的是给大家传达一种观念

机器永远是对的

计算机系统的行为是按照官方手册的描述精确发生的

  • 每一次状态转移都有手册依据
  • 如果你不理解计算机系统的行为, 很大概率是因为你不了解相关手册中的某些关键细节
    • 这对初学者会有一定压力(哪都不太了解)
      • 但只有树立正确的观念, 才能解决问题
  • 永远的理解: 商业产品也会有bug(Intel奔腾的fdiv bug), 但你恰好遇上的概率很小
    • 不要凭空质疑, 需要有证据(高中议论文的训练)

迈向现代计算机系统

从一到无穷大

  1. 添加减法指令, 程序就能进行减法操作
    • 补码加法器可进行减法操作
  2. 实现乘法
    • 通过跳转指令实现的循环来重复执行加法指令
  3. 实现除法
    • 通过跳转指令实现的循环来重复执行减法指令
  4. 实现小数的表示
    • \(x=1.\alpha\times 2^\beta\), \(\alpha\)为二进制无符号数, \(\beta\)为二进制有符号数
  5. 实现小数的四则运算
    • 转换成\(\alpha\)\(\beta\)相关的四则运算
  6. 实现幂级数的计算
    • 分解成四则运算和循环

从一到无穷大(2)

  1. 实现基本初等函数的计算(常函数, 幂函数, 指数函数, 对数函数, 三角函数, 反三角函数)
    • 除了常函数, 其他都可以通过展开成幂级数进行计算
  2. 实现复合函数的计算
    • 通过程序中的函数调用功能
  3. 实现初等函数的计算
    • 通过基本初等函数之间的有限次有理运算和复合操作
  4. 实现导数的计算
    • 代入一个很小的\(\Delta x\), 就能近似计算出函数在某一点的导数
  5. 实现积分的计算
    • 将被积区间分成足够多份, 用循环计算黎曼和
  6. 实现复数的四则运算
    • 分别对实数分量和虚数分量进行计算, 再处理虚数单位\(i\)

从一到无穷大(3)

  1. 实现复数的导数和积分
    • 和实数类似
  2. 实现各种物理引擎
    • 根据物理公式进行计算

 

用户在计算机中感受到的一切, 都是通过程序执行一条条指令来实现的!

更快, 更强

为了实现更高效的计算过程, 现代ISA提供更多指令:

  • 加减乘除, 逻辑运算(与或非), 浮点指令, 原子指令…
  • 一些ISA甚至还提供直接计算初等函数的指令
    • 如开方指令, 三角函数指令等

 

计算机架构师还致力于设计出能更高效执行指令的计算机:

  • 通过流水线技术提升计算机执行指令的吞吐
  • 通过超标量技术让计算机在一个周期内执行多条指令
  • 通过缓存技术提升计算机访问内存的效率
  • 通过GPU等处理器加速特定任务的执行效率

 

让计算机用户获得越来越快的性能体验

一个常见的误区: 指令集 vs. 处理器

指令集和处理器是不同层次的概念

  • 指令集和处理器都各自有3种知识产权模式
    • 开放免费(Open & Free) - 不花钱就能用
    • 可授权(Licensable) - 花钱就能用
    • 封闭(Closed) - 花钱也不能用

下图取自《关于RISC-V和开源处理器的一些解读》

RISC-V: 标准和实现分离, 企业竞争的是如何设计出更好的处理器

正确理解开放指令集和开源处理器

  • 封闭指令集不可能做出开源处理器(矩阵的左下三角)
    • 课题组中的真实故事: 证明了开放免费的ARM处理器是不存在的
  • 开放指令集并不意味着做出的处理器必须开源(矩阵的右上三角)
  • 过去只有可授权和封闭模式, 非专业人士可能只看到矩阵的对角线

包云岗老师对文章《点评RISC-V芯片出货量突破100亿》的补充评论:

RISC-V好比处理器领域的马克思主义,RISC-V手册好比《共产党宣言》,全世界各国都可以获取(即使是被
极限制裁的俄罗斯),关键是谁能实践好,也就是参照RISC-V手册做出高水平的处理器芯片,进而影响和主导
RISC-V的后续演进。

自测题

出自《从技术的角度来看,RISC-V 能对芯片发展、科技自主起到哪些作用?》

  • X86是一种指令集规范?
  • 苹果M1牛是因为采用了ARM指令集?
  • 国产处理器的实现和国外还有差距?
  • 几个月就可以定义一个新指令集?
  • 根据指令集规范实现一个处理器不容易?
  • 给定一个指令集只有一种处理器实现?
  • 可以给一个处理器实现换一个指令集?

 

答案: Yes, No, Yes, Yes, Yes, No, Yes

更多参考资料: 《4年21份资料10万字:记录RISC-V在中国的一条轨迹》

总会有缺乏专业素质的自媒体哗众取宠

 

《俄数字发展部部长:将大力扶持国产 RISC-V 处理器发展》

  • 俄罗斯部长脑抽被谎言骗了? 🙃

 

 

推荐参加一生一芯补充基础知识, 零基础亦可学习

开放讨论: 如果哪天RISC-V标准不再开放, 怎么办?

这个问题本质上是对基于开放标准的治理机制理解不到位

  • 法律上: 难以单方面将开放的标准修改为封闭
    • RISC-V是ISA, 其载体是指令集手册, 采用BSD宽松开源协议发布
    • 允许任何组织或个人免费使用, 修改和分发, 且无需公开衍生设计的专有实现细节
  • 动机上: 改为封闭不符合基金会的利益
    • 一个随时可能改为封闭的开放标准, 将失去全球企业的信任
    • 对标准自身来说, 是自毁长城

总结

计算机系统都是状态机

C程序 ISA CPU
状态 \(\{(PC, V)\}\) \(\{(PC, R, M)\}\) \(\{时序逻辑电路\}\)
状态转移规则 C语言语句的语义 指令的语义 组合逻辑电路
FM C语言标准手册 指令集手册 架构设计文档

 

  • 程序编译 = 将语句翻译成语义等价的指令序列
  • 架构设计 = 按照指令语义设计行为等价的电路
  • 程序运行 = 指令序列驱动电路进行状态转移

 

  • 机器永远是对的
    • 不要凭空质疑, 需要有证据