实验准备
实验环境:Ubuntu 24.04.3 LTS
按照说明编译工具
Y86-64 工具链概览
刚编译的四个工具的作用:
| 工具 | 文件位置 | 功能 | 输入 | 输出 |
|---|---|---|---|---|
| yas | misc/yas |
Y86-64汇编器 | .ys 汇编源文件 |
.yo 目标文件(机器码) |
| yis | misc/yis |
ISA模拟器(参考实现) | .yo 目标文件 |
执行结果(寄存器/内存状态) |
| ssim | seq/ssim |
顺序处理器模拟器 | .yo 目标文件 |
逐周期执行结果 |
| psim | pipe/psim |
流水线处理器模拟器 | .yo 目标文件 |
逐周期执行结果(5级流水线) |
工作流程:
.ys 源程序 ──[yas]──> .yo 机器码 ──┬──[yis]──> 正确的结果(参考)
├──[ssim]──> 顺序处理器结果
└──[psim]──> 流水线处理器结果
对比验证 yis 和 psim 是否一致
Part A - 编写Y86-64程序
实验要求(源自实验说明文档)
概述
在本部分中,编写三个Y86-64程序实现链表操作和数据块拷贝,深入理解x86-64调用约定、内存管理和处理器模拟。
三个任务概览:
| 程序 | 功能 | 数据结构 | 执行步骤 | 预期结果 |
|---|---|---|---|---|
| sum.ys | 链表迭代求和 | 单向链表(3元素) | 25步 | %rax = 0xcba |
| rsum.ys | 链表递归求和 | 单向链表(3元素) | 39步 | %rax = 0xcba |
| copy.ys | 数据块拷贝+XOR | 数组拷贝(3元素) | 35步 | %rax = 0xcba |
关键要求:
- 遵循x86-64调用约定(%rdi、%rsi、%rdx传递参数)
- 正确使用栈进行函数调用和局部变量存储
- 保存和恢复被调用方保存的寄存器(callee-save)
C 语言参考实现
以下是三个程序的 C 语言版本(来自 examples.c):
c
/* 链表元素结构 */
typedef struct ELE {
long val;
struct ELE *next;
} *list_ptr;
/* sum_list - 迭代求和链表元素 */
long sum_list(list_ptr ls) {
long val = 0;
while (ls) {
val += ls->val;
ls = ls->next;
}
return val;
}
/* rsum_list - 递归求和链表元素 */
long rsum_list(list_ptr ls) {
if (!ls)
return 0;
else {
long val = ls->val;
long rest = rsum_list(ls->next);
return val + rest;
}
}
/* copy_block - 复制数据块并计算校验和 */
long copy_block(long *src, long *dest, long len) {
long result = 0;
while (len > 0) {
long val = *src++;
*dest++ = val;
result ^= val;
len--;
}
return result;
}
测试验证
所有三个程序使用相同的数据集进行测试,确保结果可比较:
数据结构:
yaml
链表元素结构 (8字节值 + 8字节指针):
ele1 → val=0x00a(10), next→ele2
ele2 → val=0x0b0(176), next→ele3
ele3 → val=0xc00(3072),next=NULL
期望求和:0x00a + 0x0b0 + 0xc00 = 0xcba (3258)
期望XOR: 0x00a ^ 0x0b0 ^ 0xc00 = 0xcba (3258)
验证方法:
bash
# 汇编
./sim/misc/yas ./PartA/sum.ys # 生成sum.yo
./sim/misc/yas ./PartA/rsum.ys # 生成rsum.yo
./sim/misc/yas ./PartA/copy.ys # 生成copy.yo
# 验证(ISA参考实现)
./sim/misc/yis ./PartA/sum.yo # 应输出: 0xcba
./sim/misc/yis ./PartA/rsum.yo # 应输出: 0xcba
./sim/misc/yis ./PartA/copy.yo # 应输出: 0xcba
# GUI可视化调试
cd ./sim/seq && ./ssim -g ../../PartA/sum.yo # 交互式调试
任务 1: sum.ys(链表迭代求和)
功能描述: 实现C语言中sum_list函数的Y86-64版本,使用迭代方式遍历链表
算法流程:
yaml
sum_list(ls):
val = 0
while (ls != NULL):
val += ls->val # 累加当前节点的值
ls = ls->next # 移动到下一个节点
return val
Y86-64实现详解:
| 指令段 | 功能 | 关键代码 |
|---|---|---|
| 初始化 | 设置栈、参数 | irmovq stack,%rsp 初始化栈指针 irmovq ele1,%rdi 传递链表首地址 |
| sum函数 | 迭代求和逻辑 | irmovq $0,%rax 初始化累加器 andq %rdi,%rdi 测试指针非空 jmp test 跳转到循环条件测试 |
| loop标签 | 循环体 | mrmovq (%rdi),%r9 读取val addq %r9,%rax 累加 mrmovq 8(%rdi),%rdi 读取next |
| test标签 | 循环条件 | jne loop 如果指针非NULL继续循环 |
Y86-64代码:
yaml
# sum.ys - 链表迭代求和
# 作者: JianBai Sun (1320240204)
# 功能: 对链表元素进行迭代求和
.pos 0
irmovq stack,%rsp # 初始化栈指针
irmovq ele1,%rdi # %rdi = 首元素地址(参数1)
call sum # 调用sum函数
halt # 停机
.align 8
# 链表数据: 每个节点16字节(8字节值 + 8字节指针)
ele1:
.quad 0x00a # 节点1值: 10
.quad ele2 # 节点1的next指针
ele2:
.quad 0x0b0 # 节点2值: 176
.quad ele3 # 节点2的next指针
ele3:
.quad 0xc00 # 节点3值: 3072
.quad 0 # 节点3的next指针(NULL)
# sum函数: 迭代求和
sum:
irmovq $0,%rax # %rax = 累加器 (初始为0)
irmovq $8,%r8 # %r8 = 8 (节点值在偏移0处)
andq %rdi,%rdi # 测试%rdi是否为NULL
jmp test # 跳转到循环条件
loop:
mrmovq (%rdi),%r9 # %r9 = 当前节点的值 [%rdi+0]
addq %r9,%rax # %rax += %r9 (累加)
mrmovq 8(%rdi),%rdi # %rdi = 下一个节点 [%rdi+8]
andq %rdi,%rdi # 更新条件码(ZF=1 if %rdi==0)
test:
jne loop # 如果指针非NULL, 继续循环
ret # 返回(%rax保存结果)
.pos 0x200
stack: # 栈起始地址
执行结果验证:
bash
$ ./sim/misc/yis ./PartA/sum.yo
Stopped in 25 steps at PC = 0x1d. Status 'HLT', CC Z=1 S=0 O=0
Changes to registers:
%rax: 0x0000000000000000 → 0x0000000000000cba ✓ 正确
%rsp: 0x0000000000000000 → 0x0000000000000200 ✓ 栈初始化
%r8: 0x0000000000000000 → 0x0000000000000008
%r9: 0x0000000000000000 → 0x0000000000000c00 (最后一个值)
Changes to memory:
0x01f8: 0x0000000000000000 → 0x000000000000001d (返回地址)
正确性分析:
- 执行25步,符合预期
- 最终%rax = 0xcba = 10 + 176 + 3072 = 3258 ✓
- 栈指针正确初始化到0x200
- 条件码Z=1表示最后读取NULL指针,正确终止循环
任务 2: rsum.ys(链表递归求和)
功能描述: 实现C语言中rsum_list函数的Y86-64版本,使用递归方式求和
算法流程:
yaml
rsum_list(ls):
if (ls == NULL):
return 0
else:
val = ls->val
rest = rsum_list(ls->next) # 递归调用
return val + rest
递归机制分析:
- 每次递归调用会压入返回地址和%rax
- 栈帧结构:返回地址 上层%rax值
- 递归深度 = 链表长度 = 3层
Y86-64实现详解:
| 指令段 | 功能 | 关键代码 |
|---|---|---|
| 初始化 | 设置栈、参数 | irmovq $0,%rax 初始化返回值 irmovq ele1,%rdi 传递链表首地址 |
| rsum函数 | 递归求和逻辑 | andq %rdi,%rdi 测试基础情况 jmp test 跳转到条件测试 |
| calc标签 | 递归调用 | pushq %rax 保存返回值 mrmovq (%rdi),%rax 读取当前值 mrmovq 8(%rdi),%rdi 指向下一节点 call rsum 递归调用 |
| 返回处理 | 合并结果 | popq %r8 恢复上层返回值 addq %r8,%rax 累加结果 |
Y86-64代码:
yaml
# rsum.ys - 链表递归求和
# 作者: JianBai Sun (1320240204)
# 功能: 对链表元素进行递归求和
.pos 0
irmovq stack,%rsp # 初始化栈指针
irmovq ele1,%rdi # %rdi = 首元素地址
irmovq $0,%rax # %rax = 返回值累加器
call rsum # 调用rsum函数
halt # 停机
.align 8
ele1:
.quad 0x00a # 节点1值: 10
.quad ele2 # 节点1的next指针
ele2:
.quad 0x0b0 # 节点2值: 176
.quad ele3 # 节点2的next指针
ele3:
.quad 0xc00 # 节点3值: 3072
.quad 0 # 节点3的next指针(NULL)
# rsum函数: 递归求和
rsum:
andq %rdi,%rdi # 测试%rdi == NULL (基础情况)
jmp test # 跳转到条件判断
calc:
pushq %rax # 保存上层%rax值到栈
mrmovq (%rdi),%rax # %rax = 当前节点值
mrmovq 8(%rdi),%rdi # %rdi = 下一节点(参数)
call rsum # 递归: rsum(ls->next)
# 返回后: %rax = rest的和
popq %r8 # %r8 = 恢复的上层值
addq %r8,%rax # %rax = 上层val + rest
ret # 返回
test:
jne calc # 如果%rdi != NULL, 进行递归
ret # 否则返回(基础情况: %rax=0)
.pos 0x200
stack: # 栈起始地址
执行结果验证:
bash
$ ./sim/misc/yis ./PartA/rsum.yo
Stopped in 39 steps at PC = 0x27. Status 'HLT', CC Z=0 S=0 O=0
Changes to registers:
%rax: 0x0000000000000000 → 0x0000000000000cba ✓ 正确
%rsp: 0x0000000000000000 → 0x0000000000000200 ✓ 栈平衡
Changes to memory:
0x01c8: 0x0000000000000000 → 0x0000000000000082 (递归1: 返回地址)
0x01d0: 0x0000000000000000 → 0x00000000000000b0 (递归1: val=176)
0x01d8: 0x0000000000000000 → 0x0000000000000082 (递归2: 返回地址)
0x01e0: 0x0000000000000000 → 0x000000000000000a (递归2: val=10)
0x01e8: 0x0000000000000000 → 0x0000000000000082 (递归3: 返回地址)
0x01f8: 0x0000000000000000 → 0x0000000000000027 (主程序: 返回地址)
正确性分析:
- 最终%rax = 0xcba = 0x00a + 0x0b0 + 0xc00 = 3258 ✓
- 栈内存显示3层递归调用的栈帧
- 栈指针恢复到0x200,栈平衡正确
- 递归深度 = 3(链表长度),符合预期
递归执行流程追踪:
主程序: rsum(ele1)
├─ rsum(ele1->next=ele2):
│ ├─ rsum(ele2->next=ele3):
│ │ ├─ rsum(ele3->next=NULL):
│ │ │ return 0 # 基础情况
│ │ return 0xc00 + 0 = 0xc00 # 返回
│ return 0x0b0 + 0xc00 = 0xcb0 # 返回
return 0x00a + 0xcb0 = 0xcba # 返回
最终结果: 0x00a + 0x0b0 + 0xc00 = 0xcba ✓
任务 3: copy.ys(数据块拷贝)
功能描述: 实现C语言中copy_block函数的Y86-64版本,复制数据块并计算XOR校验和
算法流程:
yaml
copy_block(src, dest, len):
result = 0
while (len > 0):
val = *src++ # 读取源数据
*dest++ = val # 写入目标
result ^= val # XOR累加校验和
len-- # 计数器递减
return result
参数传递约定(x86-64):
- %rdi = src(源地址)
- %rsi = dest(目标地址)
- %rdx = len(元素个数)
Y86-64实现详解:
| 指令段 | 功能 | 关键代码 |
|---|---|---|
| 初始化 | 设置参数和临时变量 | irmovq $0,%rax result=0 irmovq $8,%r10 每次步进8字节 |
| copy函数 | 数据拷贝逻辑 | mrmovq (%rdi),%r11 读取src rmmovq %r11,(%rsi) 写入 dest addq %r10,%rdi/%rsi src++, dest++ |
| 校验计算 | XOR校验 | xorq %r11,%rax result ^= val |
| 循环控制 | 计数器管理 | subq %r9,%rdx len-- jg loop len > 0继续 |
Y86-64代码:
yaml
# copy.ys - 数据块拷贝并计算XOR校验和
# 作者: JianBai Sun (1320240204)
# 功能: 复制数据块并计算校验和
# 参数: %rdi=src, %rsi=dest, %rdx=len
# 返回: %rax=XOR校验和
.pos 0
irmovq stack, %rsp # 初始化栈指针
irmovq src,%rdi # %rdi = 源地址(参数1)
irmovq dest,%rsi # %rsi = 目标地址(参数2)
irmovq $3,%rdx # %rdx = 元素个数(参数3)
call copy # 调用copy函数
halt # 停机
.align 8
src:
.quad 0x00a # 源数据[0] = 10
.quad 0x0b0 # 源数据[1] = 176
.quad 0xc00 # 源数据[2] = 3072
dest:
.quad 0x111 # 初始目标[0] = 273
.quad 0x222 # 初始目标[1] = 546
.quad 0x333 # 初始目标[2] = 819
# copy函数: 拷贝数据并计算XOR校验
copy:
irmovq $0,%rax # %rax = result = 0
irmovq $0,%r8 # %r8 = 临时值0(用于subq)
irmovq $1,%r9 # %r9 = 步进值1(用于len--)
irmovq $8,%r10 # %r10 = 8(指针步进)
subq %r8,%rdx # 标志位准备(实际不改变%rdx)
jmp test # 跳转到循环条件
loop:
mrmovq (%rdi),%r11 # %r11 = *src (读源数据)
addq %r10,%rdi # src++ (src += 8)
rmmovq %r11,(%rsi) # *dest = %r11 (写目标数据)
addq %r10,%rsi # dest++ (dest += 8)
xorq %r11,%rax # result ^= %r11 (XOR校验)
subq %r9,%rdx # len-- (len -= 1)
test:
jg loop # 如果len > 0, 继续循环
ret # 返回(%rax=校验和)
.pos 0x200
stack: # 栈起始地址
执行结果验证:
bash
$ ./sim/misc/yis ./PartA/copy.yo
Stopped in 35 steps at PC = 0x31. Status 'HLT', CC Z=1 S=0 O=0
Changes to registers:
%rax: 0x0000000000000000 → 0x0000000000000cba ✓ XOR校验和正确
%rsp: 0x0000000000000000 → 0x0000000000000200 ✓ 栈平衡
%rsi: 0x0000000000000000 → 0x0000000000000068 (dest指向末位+8)
%rdi: 0x0000000000000000 → 0x0000000000000050 (src指向末位+8)
%r9: 0x0000000000000000 → 0x0000000000000001
%r10: 0x0000000000000000 → 0x0000000000000008
%r11: 0x0000000000000000 → 0x0000000000000c00 (最后读取的值)
Changes to memory (数据拷贝结果):
0x0050: 0x0000000000000111 → 0x000000000000000a ✓ src[0]拷贝成功
0x0058: 0x0000000000000222 → 0x00000000000000b0 ✓ src[1]拷贝成功
0x0060: 0x0000000000000333 → 0x0000000000000c00 ✓ src[2]拷贝成功
0x01f8: 0x0000000000000000 → 0x0000000000000031 (返回地址)
正确性分析:
-
执行35步,符合预期
-
最终%rax = 0xcba = 0x00a ^ 0x0b0 ^ 0xc00 = 3258 ✓
验算: 0x00a ^ 0x0b0 = 0x0ba 0x0ba ^ 0xc00 = 0xcba ✓ -
内存0x50-0x60处的数据被正确拷贝:
- dest[0]: 0x111 → 0x00a ✓
- dest[1]: 0x222 → 0x0b0 ✓
- dest[2]: 0x333 → 0xc00 ✓
-
指针%rdi和%rsi正确步进到末位+8
-
栈指针恢复到0x200
Part B - SEQ处理器扩展
实验要求
任务概述
本部分在目录 sim/seq 中进行。任务是扩展 SEQ 处理器以支持 iaddq 指令。通过修改文件 seq-full.hcl(实现 CS:APP3e 教科书中描述的 SEQ 版本),实现新指令的硬件控制逻辑。该文件包含了解决方案所需的指令编码常量声明。
iaddq指令简介
iaddq 是一条立即数加法指令,结合了 irmovq(立即数传送)和 addq(加法)的功能。
指令格式和功能:
iaddq V, rB- 将立即数 V 加到寄存器 rB,结果存回 rB- 与
irmovq V, rT后跟addq rT, rB的效果相同 - 参考 CS:APP3e 教材图 4.18 中 irmovq 和 OPq 的描述实现
示例代码(来自 sim/y86-code/asumi.ys):
yaml
sum:
xorq %rax,%rax # sum = 0
andq %rsi,%rsi # Set condition codes
jmp test
loop:
mrmovq (%rdi),%r10 # Get *start
addq %r10,%rax # Add to sum
iaddq $8,%rdi # start++ <- iaddq指令示例
iaddq $-1,%rsi # count-- <- iaddq指令示例
test:
jne loop # Stop when 0
ret
HCL文件修改要求
修改的 HCL 文件必须以注释头开始,包含以下信息:
- 实验者的姓名和学号
- iaddq 指令所需计算的详细描述
- 取指阶段:从内存读取指令码和立即数
- 译码阶段:识别 rB 寄存器
- 执行阶段:执行加法操作(ALU)
- 访存阶段:不需要内存访问
- 写回阶段:将结果写入 rB 寄存器
- 更新PC阶段:PC = PC + 10(10字节指令长度)
构建和测试解决方案
1. 修改 seq-full.hcl
修改 seq-full.hcl 文件中的相关部分,添加 iaddq 指令的硬件逻辑。该文件已包含 iaddq 的指令编码声明:
verilog
wordsig IIADDQ 'I_IADDQ' # Instruction code for iaddq instruction
2. 编译新的模拟器
修改完成后,编译新的 SEQ 模拟器:
bash
cd ./sim/seq
make VERSION=full
此命令基于修改的 seq-full.hcl 文件构建新的 ssim 二进制文件。
3. 基础功能测试
在 TTY 模式下测试简单程序,与 ISA 模拟器的结果进行对比:
bash
./ssim -t ../y86-code/asumi.yo
如果出现错误,可在 GUI 模式下逐步调试实现:
bash
./ssim -g ../y86-code/asumi.yo
4. 基准程序测试
验证对原有指令的兼容性,确保修改未破坏已有功能:
bash
cd ../y86-code
make testssim
该命令在基准程序上运行 ssim,通过比对处理器状态与 ISA 模拟器的结果来验证正确性。这些程序不包含 iaddq 指令,仅用于回归测试。详见 sim/y86-code/README 文件。
5. 回归测试
执行完整的回归测试套件验证实现质量。
不含 iaddq 指令的回归测试:
bash
cd ../ptest
make SIM=../seq/ssim
包含 iaddq 指令的回归测试:
bash
cd ../ptest
make SIM=../seq/ssim TFLAGS=-i
参考资源
详细的 SEQ 模拟器说明见 CS:APP3e Y86-64 处理器模拟器指南(simguide.pdf)。
实验内容
指令跟踪
根据iaddq指令的功能(将立即数V加到寄存器rB),首先对其在SEQ各个阶段进行指令跟踪分析:
| 阶段 | iaddq V, rB 执行的操作 | 说明 |
|---|---|---|
| 取指 | icode:ifun ← M₁[PC] |
从内存读取指令码和功能码 |
rA:rB ← M₁[PC+1] |
读取寄存器字节(rA未使用,rB为目标寄存器) | |
valC ← M₈[PC+2] |
读取8字节立即数V | |
valP ← PC + 10 |
计算下一条指令地址(10字节指令长度) | |
| 译码 | valB ← R[rB] |
读取rB寄存器的当前值 |
| 执行 | valE ← valC + valB |
ALU执行加法:立即数 + 寄存器值 |
Set CC |
更新条件码(ZF、SF、OF) | |
| 访存 | --- | 无需访存操作 |
| 写回 | R[rB] ← valE |
将ALU结果写回rB寄存器 |
| 更新PC | PC ← valP |
更新程序计数器 |
设计思路:
iaddq指令融合了irmovq和addq的特性:
- 取指/译码 :类似
irmovq,需要读取立即数和寄存器字段 - 执行 :类似
addq,执行ALU加法并更新条件码 - 写回:将结果写回rB,无条件执行(不像cmovXX需要判断条件)
修改HCL表达式
根据指令跟踪分析,需要在7个关键位置的HCL表达式中添加IIADDQ支持:
1. 取指阶段修改
(1)指令有效性验证
verilog
bool instr_valid = icode in
{ INOP, IHALT, IRRMOVQ, IIRMOVQ, IRMMOVQ, IMRMOVQ,
IOPQ, IJXX, ICALL, IRET, IPUSHQ, IPOPQ, IIADDQ }; // ← 添加IIADDQ
修改原因: 将iaddq声明为合法指令,避免触发无效指令异常(SINS)。
(2)需要寄存器字节
verilog
bool need_regids =
icode in { IRRMOVQ, IOPQ, IPUSHQ, IPOPQ,
IIRMOVQ, IRMMOVQ, IMRMOVQ, IIADDQ }; // ← 添加IIADDQ
修改原因: iaddq指令格式包含rB字段(第2字节),需要取指逻辑解析寄存器编号。
(3)需要立即数常量
verilog
bool need_valC =
icode in { IIRMOVQ, IRMMOVQ, IMRMOVQ, IJXX, ICALL, IIADDQ }; // ← 添加IIADDQ
修改原因: iaddq的立即数V占用第3-10字节,需要读取8字节常量到valC。
2. 译码阶段修改
(4)B源寄存器
verilog
word srcB = [
icode in { IOPQ, IRMMOVQ, IMRMOVQ, IIADDQ } : rB; // ← 添加IIADDQ
icode in { IPUSHQ, IPOPQ, ICALL, IRET } : RRSP;
1 : RNONE;
];
修改原因: 需要读取rB的当前值到valB,作为ALU的一个操作数(被加数)。
3. 写回阶段修改
(5)E目标寄存器
verilog
word dstE = [
icode in { IRRMOVQ } && Cnd : rB;
icode in { IIRMOVQ, IOPQ, IIADDQ } : rB; // ← 添加IIADDQ
icode in { IPUSHQ, IPOPQ, ICALL, IRET } : RRSP;
1 : RNONE;
];
修改原因: 指定ALU计算结果(valE)写回rB寄存器。
4. 执行阶段修改
(6)ALU的A输入
verilog
word aluA = [
icode in { IRRMOVQ, IOPQ } : valA;
icode in { IIRMOVQ, IRMMOVQ, IMRMOVQ, IIADDQ } : valC; // ← 添加IIADDQ
icode in { ICALL, IPUSHQ } : -8;
icode in { IRET, IPOPQ } : 8;
];
修改原因: 将立即数valC作为ALU第一个操作数(加数)。
(7)ALU的B输入
verilog
word aluB = [
icode in { IRMMOVQ, IMRMOVQ, IOPQ, ICALL,
IPUSHQ, IRET, IPOPQ, IIADDQ } : valB; // ← 添加IIADDQ
icode in { IRRMOVQ, IIRMOVQ } : 0;
];
修改原因: 将rB的值(valB)作为ALU第二个操作数(被加数)。
(8)条件码更新
verilog
bool set_cc = icode in { IOPQ, IIADDQ }; // ← 添加IIADDQ
修改原因: iaddq执行加法后需要更新条件码(ZF、SF、OF),以支持后续条件跳转指令(如jne)的判断。这是最容易遗漏的修改点。
示例说明:
yaml
iaddq $-1, %rsi # count-- (更新条件码)
jne loop # 如果count≠0继续循环(依赖ZF标志)
HCL修改总结
完整的修改涉及8个表达式,覆盖了SEQ的5个阶段:
| 修改位置 | 表达式 | 添加内容 | 对应阶段 |
|---|---|---|---|
| 1 | instr_valid |
IIADDQ |
取指 |
| 2 | need_regids |
IIADDQ |
取指 |
| 3 | need_valC |
IIADDQ |
取指 |
| 4 | srcB |
IIADDQ |
译码 |
| 5 | dstE |
IIADDQ |
写回 |
| 6 | aluA |
IIADDQ |
执行 |
| 7 | aluB |
IIADDQ |
执行 |
| 8 | set_cc |
IIADDQ |
执行 |
实验结果
基础功能测试:
bash
(base) sun@zzz:~/codes/CSAPP/第四章实验-优化Y86-64流水线处理器性能$ cd sim/seq && make VERSION=full
# Building the seq-full.hcl version of SEQ
../misc/hcl2c -n seq-full.hcl <seq-full.hcl >seq-full.c
gcc -Wall -O2 -fcommon -Wl,--no-as-needed -isystem /usr/include/tcl8.6 -I../misc -DHAS_GUI -o ssim \
seq-full.c ssim.c ../misc/isa.c -L/usr/lib -ltk -ltcl -lm
(base) sun@zzz:~/codes/CSAPP/第四章实验-优化Y86-64流水线处理器性能/sim/seq$ ./ssim -t ../y86-code/asumi.yo
Y86-64 Processor: seq-full.hcl
137 bytes of code read
IF: Fetched irmovq at 0x0. ra=----, rb=%rsp, valC = 0x100
IF: Fetched call at 0xa. ra=----, rb=----, valC = 0x38
Wrote 0x13 to address 0xf8
IF: Fetched irmovq at 0x38. ra=----, rb=%rdi, valC = 0x18
IF: Fetched irmovq at 0x42. ra=----, rb=%rsi, valC = 0x4
IF: Fetched call at 0x4c. ra=----, rb=----, valC = 0x56
Wrote 0x55 to address 0xf0
IF: Fetched xorq at 0x56. ra=%rax, rb=%rax, valC = 0x0
IF: Fetched andq at 0x58. ra=%rsi, rb=%rsi, valC = 0x0
IF: Fetched jmp at 0x5a. ra=----, rb=----, valC = 0x83
IF: Fetched jne at 0x83. ra=----, rb=----, valC = 0x63
IF: Fetched mrmovq at 0x63. ra=%r10, rb=%rdi, valC = 0x0
IF: Fetched addq at 0x6d. ra=%r10, rb=%rax, valC = 0x0
IF: Fetched iaddq at 0x6f. ra=----, rb=%rdi, valC = 0x8
IF: Fetched iaddq at 0x79. ra=----, rb=%rsi, valC = 0xffffffffffffffff
IF: Fetched jne at 0x83. ra=----, rb=----, valC = 0x63
IF: Fetched mrmovq at 0x63. ra=%r10, rb=%rdi, valC = 0x0
IF: Fetched addq at 0x6d. ra=%r10, rb=%rax, valC = 0x0
IF: Fetched iaddq at 0x6f. ra=----, rb=%rdi, valC = 0x8
IF: Fetched iaddq at 0x79. ra=----, rb=%rsi, valC = 0xffffffffffffffff
IF: Fetched jne at 0x83. ra=----, rb=----, valC = 0x63
IF: Fetched mrmovq at 0x63. ra=%r10, rb=%rdi, valC = 0x0
IF: Fetched addq at 0x6d. ra=%r10, rb=%rax, valC = 0x0
IF: Fetched iaddq at 0x6f. ra=----, rb=%rdi, valC = 0x8
IF: Fetched iaddq at 0x79. ra=----, rb=%rsi, valC = 0xffffffffffffffff
IF: Fetched jne at 0x83. ra=----, rb=----, valC = 0x63
IF: Fetched mrmovq at 0x63. ra=%r10, rb=%rdi, valC = 0x0
IF: Fetched addq at 0x6d. ra=%r10, rb=%rax, valC = 0x0
IF: Fetched iaddq at 0x6f. ra=----, rb=%rdi, valC = 0x8
IF: Fetched iaddq at 0x79. ra=----, rb=%rsi, valC = 0xffffffffffffffff
IF: Fetched jne at 0x83. ra=----, rb=----, valC = 0x63
IF: Fetched ret at 0x8c. ra=----, rb=----, valC = 0x0
IF: Fetched ret at 0x55. ra=----, rb=----, valC = 0x0
IF: Fetched halt at 0x13. ra=----, rb=----, valC = 0x0
32 instructions executed
Status = HLT
Condition Codes: Z=1 S=0 O=0
Changed Register State:
%rax: 0x0000000000000000 0x0000abcdabcdabcd
%rsp: 0x0000000000000000 0x0000000000000100
%rdi: 0x0000000000000000 0x0000000000000038
%r10: 0x0000000000000000 0x0000a000a000a000
Changed Memory State:
0x00f0: 0x0000000000000000 0x0000000000000055
0x00f8: 0x0000000000000000 0x0000000000000013
ISA Check Succeeds
基准程序测试:
bash
(base) sun@zzz:~/codes/CSAPP/第四章实验-优化Y86-64流水线处理器性能/sim/y86-code$ make testssim
Makefile:42: 警告:忽略后缀规则定义的先决条件
Makefile:45: 警告:忽略后缀规则定义的先决条件
Makefile:48: 警告:忽略后缀规则定义的先决条件
Makefile:51: 警告:忽略后缀规则定义的先决条件
../seq/ssim -t asum.yo > asum.seq
../seq/ssim -t asumr.yo > asumr.seq
../seq/ssim -t cjr.yo > cjr.seq
../seq/ssim -t j-cc.yo > j-cc.seq
../seq/ssim -t poptest.yo > poptest.seq
../seq/ssim -t pushquestion.yo > pushquestion.seq
../seq/ssim -t pushtest.yo > pushtest.seq
../seq/ssim -t prog1.yo > prog1.seq
../seq/ssim -t prog2.yo > prog2.seq
../seq/ssim -t prog3.yo > prog3.seq
../seq/ssim -t prog4.yo > prog4.seq
../seq/ssim -t prog5.yo > prog5.seq
../seq/ssim -t prog6.yo > prog6.seq
../seq/ssim -t prog7.yo > prog7.seq
../seq/ssim -t prog8.yo > prog8.seq
../seq/ssim -t ret-hazard.yo > ret-hazard.seq
grep "ISA Check" *.seq
asum.seq:ISA Check Succeeds
asumr.seq:ISA Check Succeeds
cjr.seq:ISA Check Succeeds
j-cc.seq:ISA Check Succeeds
poptest.seq:ISA Check Succeeds
prog1.seq:ISA Check Succeeds
prog2.seq:ISA Check Succeeds
prog3.seq:ISA Check Succeeds
prog4.seq:ISA Check Succeeds
prog5.seq:ISA Check Succeeds
prog6.seq:ISA Check Succeeds
prog7.seq:ISA Check Succeeds
prog8.seq:ISA Check Succeeds
pushquestion.seq:ISA Check Succeeds
pushtest.seq:ISA Check Succeeds
ret-hazard.seq:ISA Check Succeeds
rm asum.seq asumr.seq cjr.seq j-cc.seq poptest.seq pushquestion.seq pushtest.seq prog1.seq prog2.seq prog3.seq prog4.seq prog5.seq prog6.seq prog7.seq prog8.seq ret-hazard.seq
回归测试(不含iaddq):
bash
(base) sun@zzz:~/codes/CSAPP/第四章实验-优化Y86-64流水线处理器性能/sim/seq$ cd ../ptest && make SIM=../seq/ssim
./optest.pl -s ../seq/ssim
Simulating with ../seq/ssim
All 49 ISA Checks Succeed
./jtest.pl -s ../seq/ssim
Simulating with ../seq/ssim
All 64 ISA Checks Succeed
./ctest.pl -s ../seq/ssim
Simulating with ../seq/ssim
All 22 ISA Checks Succeed
./htest.pl -s ../seq/ssim
Simulating with ../seq/ssim
All 600 ISA Checks Succeed
回归测试(含iaddq):
bash
(base) sun@zzz:~/codes/CSAPP/第四章实验-优化Y86-64流水线处理器性能/sim/ptest$ make SIM=../seq/ssim TFLAGS=-i
./optest.pl -s ../seq/ssim -i
Simulating with ../seq/ssim
All 58 ISA Checks Succeed
./jtest.pl -s ../seq/ssim -i
Simulating with ../seq/ssim
All 96 ISA Checks Succeed
./ctest.pl -s ../seq/ssim -i
Simulating with ../seq/ssim
All 22 ISA Checks Succeed
./htest.pl -s ../seq/ssim -i
Simulating with ../seq/ssim
All 756 ISA Checks Succeed
可以看到前面的测试全部通过,说明实验成功。
Part C - 流水线性能优化
实验要求(源自实验说明文档)
任务概述:
- 工作目录:
sim/pipe - 目标:在保持语义正确的前提下,修改
ncopy.ys和pipe-full.hcl,使ncopy.ys在 PIPE 模拟器上尽可能快地运行。
提交物:
pipe-full.hcl:如有更改(例如实现iaddq),需在文件头添加姓名、学号与修改的高层描述。ncopy.ys:函数体优化版本,文件头包含姓名、学号以及对优化思路的高层说明(修改了什么、为何这样做)。
编码规则与约束:
- 正确性:功能与 YIS 保持一致,必须正确复制非重叠数组块,并在
%rax返回src中正整数个数。 - 通用性:支持任意数组长度,禁止为固定规模(如 64 元素)硬编码展开且仅适配该规模。
- 长度限制:组装后的
ncopy.yo不得超过 1000 字节,可用check-len.pl检查。 - 回归要求:
pipe-full.hcl必须通过../y86-code与../ptest的回归测试(默认不带-i)。 - 可选扩展:允许实现
iaddq指令以降低指令数与数据相关延迟;若实现,需使用带-i的回归用例验证。
允许的优化方向:
- 指令层面:重排指令、用更高效指令替换指令组、删除冗余指令、新增有利于流水线的指令。
- 流水线友好性:指令调度以避免/隐藏 load-use 冒险,利用转发/旁路能力,减少分支开销(如循环展开+集中更新计数)。
- 结构性优化:适度循环展开(参考 CS:APP3e §5.8),批量处理多元素以提高每迭代吞吐,合并地址更新(如用
iaddq)。
构建与测试
生成驱动程序:
bash
cd sim/pipe && make clean && make VERSION=full
make drivers
- 生成
sdriver.yo(4 元素小数组)与ldriver.yo(63 元素大数组)。
构建 PIPE 模拟器:
bash
make psim VERSION=full
# 或同时重建模拟器与驱动:
make VERSION=full
在 PIPE 上运行(GUI):
bash
./psim -g sdriver.yo
./psim -g ldriver.yo
正确性测试:
bash
./correctness.pl -p
# 或者
./psim -t sdriver.yo
./psim -t ldriver.yo
# 或者
../y86-code
make testpsim
-
结束状态含义:
-
0xaaaa:全部通过 -
0xbbbb:计数错误 -
0xcccc:ncopy超过 1000 字节 -
0xdddd:源数据未正确复制 -
0xeeee:目标区域前后发生破坏
-
基准测试程序(PIPE):
bash
./benchmark.pl
实验内容
优化目标与策略
目标: 这个部分没有特别明确的实验完成的要求,但是想要得到满分需要将CPE降至7.5以下。
基线性能: 原始未优化版本的 CPE 约为 15.0+(基于 Y86-64 基础循环,每次迭代包含多次分支预测失败)。
核心优化思路:
- 循环展开:减少分支频率,提高指令级并行度(ILP)
- iaddq 指令:替代多条 irmovq + addq,减少关键路径长度
- 指令调度:利用 load-use 延迟隐藏,交错 load/store/计算指令
- 无分支计数:用条件跳转替代 cmovle,减少控制依赖开销
优化迭代过程
第一版:基线版本(CPE ≈ 15.0)
原本的ncopy.ys:
yaml
ncopy:
xorq %rax,%rax # count = 0;
andq %rdx,%rdx # len <= 0?
jle Done # if so, goto Done:
Loop:
mrmovq (%rdi), %r10 # read val from src...
rmmovq %r10, (%rsi) # ...and store it to dst
andq %r10, %r10 # val <= 0?
jle Npos # if so, goto Npos:
irmovq $1, %r10
addq %r10, %rax # count++
Npos:
irmovq $1, %r10
subq %r10, %rdx # len--
irmovq $8, %r10
addq %r10, %rdi # src++
addq %r10, %rsi # dst++
andq %rdx,%rdx # len > 0?
jg Loop # if so, goto Loop:
阅读原本的ncopy 代码,可以分析出它要做的事情是:
- 复制数据块 :将源数组
src中的所有元素逐个复制到目标数组dest - 计算正整数个数 :统计源数组
src中有多少个正整数(> 0 的元素) - 返回计数值 :将统计结果返回到寄存器
%rax
对应的伪代码是:
c
long ncopy(long *src, long *dest, long len) {
long count = 0;
while (len > 0) {
long val = *src++;
*dest++ = val; // 复制元素
if (val > 0)
count++; // 计数正整数
len--;
}
return count; // %rax = count
}
而优化的目标是:在保持功能正确的前提下,通过以下方式使 ncopy 在流水线处理器(PIPE)上运行尽可能快
- 循环展开(处理多个元素)
- 实现
iaddq指令减少指令数 - 指令调度优化以隐藏 load-use 延迟
- 优化分支预测与余数处理
初始代码的运行结果是: CPE ≈ 15.0
第二版:×2 展开版本(CPE → 11.21)
先处于稳妥起见,将循环展开 x2 试试水。
优化策略:
- 每轮处理 2 个元素,减少分支频率(1/2)
- 循环外预设常数寄存器(rOne=1, rEight=8, rZero=0)
- 连发两次
mrmovq后再使用,隐藏 load-use 延迟
代码框架:
yaml
Loop2: mrmovq (%rdi), %rcx # val0 = src[0]
mrmovq 8(%rdi), %rbx # val1 = src[1] (load发射)
rmmovq %rcx, (%rsi) # dst[0] = val0 (此时rcx已就绪)
andq %rcx, %rcx
cmovle %r10, %r11 # 若val0<=0,inc=0
addq %r11, %rax # count += inc
# ... 类似处理val1 ...
iaddq $-2, %rdx # len -= 2
jge Loop2
实际测试结果:
CPE = 11.21(平均)
最优 CPE = 9.86(n=63 时)
第三版:×8 展开 + iaddq 版本(CPE → 8.51)
既然 x2展开有效,但效果不好,所以就直接x8展开(因为实验对字节数有要求,所以不能无限制的进行循环展开,x8是当前限制下最大的展开数了)
另外,原来有很多 irmovq 写临时寄存器,再立刻被 addq/subq 读取,这样频繁读写寄存器开销很大,现在换成 iaddq 不用读这个临时寄存器,只读 rB 和立即数。
PIPE 中实现 iaddq 指令:
在 pipe-full.hcl 中添加以下修改:
verilog
# 1. 取指阶段:标记合法指令并解析操作数
bool instr_valid = f_icode in
{ INOP, IHALT, ..., IIADDQ }; # ← 添加IIADDQ
bool need_regids = f_icode in
{ ..., IIADDQ }; # ← 需要解析rB
bool need_valC = f_icode in
{ ..., IIADDQ }; # ← 需要读取立即数
# 2. 译码阶段:读取rB的当前值
word d_srcB = [
D_icode in { ..., IIADDQ } : D_rB; # ← 读rB的源值
...
];
# 3. 执行阶段:加法运算
word aluA = [
E_icode in { ..., IIADDQ } : E_valC; # ← 立即数作为A输入
...
];
word aluB = [
E_icode in { ..., IIADDQ } : E_valB; # ← rB值作为B输入
...
];
bool set_cc = E_icode in { IOPQ, IIADDQ }; # ← 更新条件码
# 4. 写回阶段:结果写回rB
word d_dstE = [
D_icode in { ..., IIADDQ } : D_rB; # ← 写回目标
...
];
×8 展开代码框架:
yaml
Loop8: iaddq $-8, %rdx # len -= 8(iaddq替代subq)
jl Remain
# 第一组load(不相邻读)
mrmovq (%rdi), %r8 # v0
mrmovq 8(%rdi), %r9 # v1
...
mrmovq 56(%rdi), %rcx # v7
# 交错store+计数(load已就绪)
rmmovq %r8, (%rsi)
andq %r8, %r8
jle L1
iaddq $1, %rax # count++(iaddq替代addq)
L1: ...(重复7次)
iaddq $64, %rdi # src += 64
iaddq $64, %rsi # dst += 64
jmp Loop8
性能改进:
- 把所有"寄存器 += 常数""寄存器 -= 常数"都从"2 条指令 + 临时寄存器"压缩成"一条 iaddq",大幅缩短循环体、降低指令数与依赖。
- 8 次循环展开,最大化内存带宽利用和流水线吞吐
实际测试结果:
CPE = 8.51(平均)
最优 CPE = 9.86
得分:39.8/60.0
第四版:×9 展开 + 优化收尾版本(CPE → 8.18)
在第三版的基础上,第四版继续提高展开,改到了9层展开,但是字节数已经非常接近上限,测试时好几次都超了
优化思路:
- 通过从 ×8 升级到 ×9 展开
- 对计数逻辑与 load/use 的先后进行重新调度,尽量隐藏加载延迟;
- 设计一套"二分式"收尾流程,使剩余元素的处理尽可能接近主循环的效率。
优化策略:
-
从 ×8 升级到 ×9 展开:
- 主循环每次处理 9 个元素,63 元素大数组刚好满足 63 = 9×7;
- 对于大规模输入,收尾分支触发次数显著减少。
-
计数逻辑与分支优化:
- 第三版中计数更新使用独立的指令序列,容易与 load-use 冒险形成竞争;
- 本版将计数更新与条件检测交错为
andq → jle Lx → iaddq $1,在分支预测正确时能更好地填补流水线空隙; - 依赖
jle跳转的即时反馈,减轻深度管线中的条件依赖带来的气泡。
-
指令调度重排:
yaml
# 原×8版(不理想):
L1: rmmovq %r9, 8(%rsi)
andq %r9, %r9 # 依赖r9
jle L2 # 依赖CC,如果预测失败→刷管道
iaddq $1, %rax
# 新版(交错):
mrmovq 16(%rdi), %r8 # 提前load下一值(不依赖r9)
rmmovq %r9, 8(%rsi)
andq %r9, %r9
jle L2
iaddq $1, %rax # 在load结果就绪前执行,隐藏延迟
- 二分收尾:
yaml
Remain:
iaddq $9, %rdx # 恢复len
R8: iaddq $-2, %rdx # 每次处理2个(而非1个)
jl R1
# ...处理2元素...
jmp R8
R1: iaddq $1, %rdx
jl Done
# ...处理1元素...
性能改进分析:
- 主循环 CPE: 约 6.5--7.0(×9 展开 + 9 次 load 隐藏延迟);
- 大 n 收尾 CPE: 约 6.5--7.0(二分收尾与主循环性能接近);
- 小 n CPE: 仍然偏高(11--14),但在整体平均中占比有限;
- 分支预测表现: 在计数路径上
jle多次连续失败后,预测器逐渐学习正确方向,减轻误预测代价。
实际测试结果:
CPE 分布:
n=9: CPE = 8.11
n=18: CPE = 7.28
n=27: CPE = 6.89
n=36: CPE = 6.78
n=45: CPE = 6.64
n=63: CPE = 6.54
平均 CPE = 8.18
最优 CPE = 6.54
得分:46.4/60.0
有少量的提升,但是不大
第五版:×8 展开 + 三叉搜索树余数优化(CPE → 7.80)
由于 x9 展开提升不大,但字节数不够。所以,第五版重新回到 ×8 展开,但着重针对"余数处理"部分进行结构性改写,使用类似"三叉搜索树"的分支结构快速定位剩余元素个数。(借鉴了一下别人的经验)
优化思路:
- 保持主循环 ×8 展开,以较低分支频率(1/8)获得稳定吞吐;
- 用两级分支判断(
jg/je/jl)直接跳转到Remain7..Remain1,用顺序直写替代"小循环收尾"; - 在余数段沿用主循环中的 load/store + 计数交错调度,尽量复用已有的流水线优化。
关键改动:
- 主循环仍为 ×8 展开,使用
iaddq合并地址与计数更新,维持分支频率为 1/8; - 余数部分通过
jg/je/jl组合判断直接跳转到对应的RemainK标签,不再使用 while 小循环逐个处理; - 在余数路径中继续采用"批量 load → 交错 store + andq/jle + iaddq"的调度方式,尽可能隐藏 load-use 冒险;
- 只有在确实存在剩余元素时才进入相应路径,减少无谓分支和误预测开销。
测试与评分(PIPE):
Average CPE 7.80
Score 54.0/60.0
效果分析:
可以看到有很大的提升
- 平均 CPE 从 8.18 降至 7.80,主要收益来自余数段取消小循环、减少分支与误预测;
- 对于较大规模数组,CPE 稳定在 6.4--6.7 之间;
- 对于较小 n,初始化与分支开销仍然占比较高,是后续优化的主要空间。
第六版:×8 展开 + Extra 风格余数优化(CPE → 7.65,Score 57.0/60.0)
第六版在第五版"三叉余数"的基础上,进一步细化余数路径中的指令序列,采用类似课本 extra 题风格的"紧凑模式",在不改变主循环结构的前提下继续压缩关键路径上的指令与气泡。
优化思路:
- 保持 ×8 展开与
iaddq的总体框架不变; - 使用
%r8/%r9交替批量mrmovq,提高 load 指令之间的并行度; - 将余数路径统一到一种"Extra 风格"的基本模板,减少特殊情况分支。
关键改动:
- 主循环仍然是 ×8 展开,
%r8/%r9交替负责加载数据,分支频率保持 1/8; - 余数部分继续依赖三叉树判定快速跳转至
Remain7..Remain1; - 在每个
RemainX段中,采用模式:mrmovq → jle ExtraX → iaddq $1,%rax → ExtraX: rmmovq → andq,使"加载、计数、存储、条件设置"紧凑排列; Remain7的首元素采用直写方式,随后也落入这一统一模板,减少重复代码结构。
测试与评分(PIPE):
- Average CPE 7.65;
- Score 57.0/60.0;
第七版:PIPE 控制逻辑微优化(执行期旁路 + load-use 放宽)(CPE → 7.65,Score 57.1/60.0)
在第七版中,不再继续修改 ncopy.ys 的汇编结构,而是从 pipe-full.hcl 的控制逻辑入手,对"加载→存储/压栈"场景下的停顿与气泡进行精细化优化。
优化思路:
- 利用执行期旁路,在特定模式下直接将访存结果转发到执行阶段,减少一次寄存器读写延迟;
- 放宽对某些模式下 load-use 冒险的停顿条件,使流水线在保证正确性的前提下更激进地前进。
关键改动(pipe-full.hcl):
verilog
# 执行期旁路 e_valA:当要用到刚加载的"数据操作数"时,直接从访存级旁路到执行级
word e_valA = [
E_icode in { IRMMOVQ, IPUSHQ } && E_srcA == M_dstM : m_valM;
1 : E_valA;
];
# 放宽 F/D/E 级的 load-use 冒险判定:
# 仅当依赖发生在 d_srcB,或依赖发生在 d_srcA 且译码指令并非 IRMMOVQ/IPUSHQ 时,才停顿/气泡。
bool F_stall =
E_icode in { IMRMOVQ, IPOPQ } &&
(E_dstM == d_srcB || (E_dstM == d_srcA && !(D_icode in { IRMMOVQ, IPUSHQ }))) ||
IRET in { D_icode, E_icode, M_icode };
bool D_stall =
E_icode in { IMRMOVQ, IPOPQ } &&
(E_dstM == d_srcB || (E_dstM == d_srcA && !(D_icode in { IRMMOVQ, IPUSHQ })));
bool E_bubble =
(E_icode == IJXX && !e_Cnd) ||
(E_icode in { IMRMOVQ, IPOPQ } &&
(E_dstM == d_srcB || (E_dstM == d_srcA && !(D_icode in { IRMMOVQ, IPUSHQ }))));
测试与评分(PIPE):
- Average CPE 7.65;
- Score 57.1/60.0;
效果分析:
- 与第六版相比,平均 CPE 基本持平,说明主路径已接近瓶颈;
- 在小规模与余数路径场景下,因停顿条件更精细,性能更稳定、波动更小。
优化总结表
| 版本 | 展开因子 | iaddq | 分支频率 | 平均CPE | 最优CPE |
|---|---|---|---|---|---|
| 基线 | 1 | ✗ | 1/1 | ~15.0 | ~15.0 |
| ×2展开 | 2 | ✗ | 1/2 | 11.21 | 9.86 |
| ×8展开+iaddq | 8 | ✓ | 1/8 | 8.51 | 9.86 |
| ×9展开+iaddq | 9 | ✓ | 1/9 | 8.18 | 6.54 |
| ×8展开+iaddq+三叉余数 | 8 | ✓ | 1/8 | 7.80 | 6.40 |
| ×8展开+iaddq+三叉余数+Extra | 8 | ✓ | 1/8 | 7.65 | 6.30--6.55 |
| ×8展开+iaddq+三叉余数+Extra+PIPE旁路 | 8 | ✓ | 1/8 | 7.65 | 6.29--6.55 |
由于花了太多时间,但最后几版的提升太小了,所以打算停住在这一版。最后也是止步在57.1分了,没能拿到满分。