决策与循环

决策与循环

默认情况下,CPU 按指令出现的顺序从上到下执行。仅凭这个顺序,它永远无法做出选择或重复工作。分支是程序离开这条直线路径的方式:它测试一个条件,当条件成立时,引导 CPU 从另一个位置继续,而不是下一行。每一个决策和每一个循环都来自这一个机制。

没有 if 或 for。你用标签(命名的位置)和分支(当条件成立时跳转到某个位置)来构造它们。标签是一个名字后跟 :。

要理解分支如何执行,你需要了解程序计数器(PC)。PC 是一个特殊寄存器,对你的代码隐藏,保存正在执行的指令的内存地址。分支指令向 PC 写入一个新值,因此 CPU 从你的目标标签而不是代码的下一行获取下一条指令。

分支家族

每个分支比较两个寄存器,并且仅在测试成立时跳转到标签。当测试失败时,执行继续到下一行。六个测试构成核心:

六个真实分支
beq a, b, label # 若 a == b 则跳转
bne a, b, label # 若 a != b 则跳转
blt a, b, label # 若 a < b 则跳转 (有符号)
bge a, b, label # 若 a >= b 则跳转 (有符号)
bltu a, b, label # 若 a < b 则跳转 (无符号)
bgeu a, b, label # 若 a >= b 则跳转 (无符号)

和 测试相等性并忽略符号。 和 构成顺序对,小于和大于或等于,并将寄存器作为有符号数比较。 和 执行相同的比较,但将值视为无符号,这适合永远不会为负的地址或计数。

有符号和无符号是什么意思?一个寄存器保存 32 位,相同的位可以用两种方式读取。作为无符号读取时,它们形成从 0 到约 40 亿的普通整数。作为有符号读取时,最高位算作负号,因此一半的位模式表示负数。位本身不会改变。改变的只是你读取它们的方式。这就是为什么有两组比较:对可能为负的普通数字使用有符号的(、),对永远不会为负的值(如内存地址)使用无符号的(、)。

没有直接的“大于则分支”或“小于或等于则分支”。你通过交换两个寄存器来获得它们,汇编器提供 和 作为伪分支来为你执行交换,因此 变成 。还有两个快捷方式: 和 将一个寄存器与零比较, 无条件跳转。

用循环求和1..5
li t0, 0 # 总和 = 0
li t1, 1 # i = 1
li t2, 6 # 上限 = 6 (当 i < 6 时循环)
loop:
bge t1, t2, done # 当 i >= 6 时退出循环
add t0, t0, t1 # 总和 += i
addi t1, t1, 1 # i++
j loop # 回去再次检查
done:
mv a0, t0 # 打印总和 (应为 15)
li a7, 1
ecall
li a7, 10
ecall

跟踪它。t0 保存运行总和(从 0 开始),t1 保存计数器 i(从 1 开始),t2 保存限制 6。每次通过 loop: 在 i 达到 6 时离开循环。否则 将 i 加到总和, 将 i 增加一, 跳回去再次测试。循环加 1、2、3、4 和 5,done 打印总和 15。

带分支的循环
loop:
bge t1, t2, done
add t0, t0, t1
addi t1, t1, 1
j loop
i (t1)1
sum (t0)0
yesnot1 < 6?sum += t1t1 += 1完成

bge 比较 t1 与 t2,以决定循环是否继续。

下图标记了更新计数器的指令和更新运行总和的指令,将它们与循环的其余部分区分开来。 推进计数器,随后的 将新的计数器值加到总和。最后一帧显示结束循环的测试。当 t1 变为 6 时, 将控制转移到 done,而不是让循环体再次运行。那个测试就是区分重复的一轮和停止的一轮的东西。
💡
用 Step 而不是 Run 推进程序。随着你前进,t1 增加,程序计数器(pc)跳回 loop。每次 pc 返回 loop,分支条件都是假的,所以循环再次运行。
⚠
如果退出条件错误,循环永远不会结束。模拟器会停止失控的循环,但分支逻辑需要你来弄对。