目录

Arch Lab实验总结

本实验中需要我们:

  • 设计和实现一个流水线化的 Y86-64 架构处理器
  • 优化处理器
  • 优化一个基准程序

本实验包括 3 个部分:

  • Part A
    • 写几个小的 Y86-64 程序
    • 熟悉 Y86-64 开发工具
  • Part B
    • 对 SEQ 模拟器扩展一个新指令
  • Part C
    • 优化 Y86-64 基准程序
    • 优化处理器设计

输入如下命令解压模拟器压缩包并且进入目录进行编译:

bash

tar xvf sim.tar
cd sim
make clean
make

编译失败,提示没有flexbison-ltk-ltcl,因此本实验需要三个软件包:

  • flex
  • bison
  • tk
  • tcl

通过如下命令安装:

bash

sudo apt install flex bison tk-dev tcl-dev

再次输入:

bash

make clean
make

编译成功,可以开始实验。

使用callq进行调用时:

  1. 压入返回地址
  2. 将 pc 修改为下一条指令地址

使用ret返回时:

  1. 弹出返回地址
  2. 将 pc 修改为返回地址

被调用者行为:

  1. 从寄存器和栈上读取值;
  2. 调用ret返回;

调用者行为:

  1. 腾出一部分栈空间;
  2. 把局部变量保存在栈上;
  3. 将<6个参数输入到寄存器中;
  4. call指令调用过程;
  5. 获取到返回值;
  6. 进行计算;
  7. 恢复栈空间;
  8. 调用ret返回;
Tip

可能需要提前保存内容到栈。

调用者行为:

  1. 腾出一部分栈空间;
  2. 部分的值的地址需要作为参数传递,那么这些值必须压在栈上,以获取地址;
  3. 从第n到第7把参数依次压在栈上,第7个在栈顶;
  4. 把其他6个参数保存到寄存器中;
  5. call指令调用过程;
  6. 从栈中获取到值,进行计算;
  7. 恢复栈空间;
  8. 调用ret返回;
Tip

在下面的示例中,被调用者同时还是个调用者。

被调用者行为:

  1. 保存之前的%rbp%rbx到栈上;
  2. 腾出一部分栈空间;
  3. 保存调用者保存寄存器的值,到被调用者保存寄存器,这样可以在调用完成后从中取出原值;(这是一种保存调用者保存寄存器值的方法)
  4. 把参数放在寄存器或者栈上,使用callq进行过程调用;
  5. %rax中取出值,进行计算;
  6. 释放栈空间;
  7. 还原调用当前过程的%rbp%rbx
  8. ret指令返回;

被调用者+调用者行为:

  1. 保存%rbx(被调用者保存寄存器);
  2. 进行过程调用和计算,返回值放到%rax中;
  3. 恢复%rbx
  4. ret指令返回;

被调用者行为:

  1. 调用者调用它时已经压入返回地址;
  2. 压入旧的%rbp
  3. %rbp = %rsp
  4. 进行常规计算,如存储局部变量等等;
  5. 计算;
  6. leave指令执行两条行为
    1. %rsp = %rbp
    2. 通过popq恢复旧的%rbp
  7. 执行ret指令返回;

综上,使用基指针和不使用基指针的差别在于:

  1. 使用基指针时,使用%rbp来确定当前栈底位置;
  2. 如果不使用,分配空间时将%rsp减去特定空间大小,返回之前就手动将%rsp加上特定空间大小

传递参数寄存器为:%rdi %rsi %rdx %rcx %r8 %r9

被调用者保存寄存器为:%rbx %rbp %r12-%r15

被调用者(同时也可能是调用者)的行为:

  1. 保存之前的%rbp%rbx到栈上;
  2. 腾出一部分栈空间;
  3. 将局部变量保存在在栈上;
  4. 保存调用者保存寄存器的值,到被调用者保存寄存器,这样可以在调用完成后从中取出原值;(这是一种保存调用者保存寄存器值的方法)
  5. 部分的值的地址需要作为参数传递,那么这些值必须压在栈上,以获取地址;
  6. 从第n到第7把参数依次压在栈上,第7个在栈顶;
  7. 把其他6个参数保存到寄存器中;
  8. call指令调用过程;
  9. 从栈中获取到值,以及从%rax中获取到返回值,进行计算;
  10. 恢复栈空间;
  11. 还原调用当前过程的%rbp%rbx
  12. 调用ret返回;

主要使用leaq计算地址,然后用movq访存求值。变长数组的地址计算不使用leaq,而使用imulq

结构体字段的写入和读取主要通过地址加上适当的偏移实现,字段的大小通过b w l q指定:

asm

; Registers: r in %rdi
movl (%rdi), %eax ;Get r->i
movl %eax, 4(%rdi) ;Store r->j

Y86-64指令集架构包括:

  • 状态单元
    • 15个程序寄存器:%rax %rcx %rdx %rbx %rsp %rbp %rsi %rdi %r8-%r14
    • 每个寄存器存储一个64位的字;
    • %rsp作为入栈、出栈、调用和返回指令的栈指针;
    • 3个一位的条件码:ZF,SF,OF
    • PC存放当前正在执行指令的地址;
    • 使用虚拟地址引用内存位置,假设虚拟内存系统向Y86-64程序提供了一个单一的字节数组映像;
    • 程序状态的最后一部分是状态码Stat,表明程序执行的总体状态;它会指示是正常运行,还是出现了某种异常;
  • 指令集
    • 只有8字节数据,遵循AT&T格式;
    • 数据传送指令:irmovq rrmovq mrmovq rmmovq
    • 整数操作指令:addq subq andq xorq,设置3个条件码ZF,SF,OF
    • 7个跳转指令:jmp jle jl je jne jge
    • 6个条件跳转指令:cmovle comvl comve comvne comvge comvg
    • callret指令
    • pushqpopq指令
    • halt指令停机
  • 编码
    • 1-10个字节不等
    • 高4位代码,低4位功能。代码为0-0xB,只有部分指令有功能字段,其他的是0;
    • 15个寄存器:0-0xE,0xF表示不访问任何寄存器
    • 使用绝对寻址;
    • 整数采用小端码编码;
    • 只有mov操作涉及到内存和立即数,其他操作均只涉及寄存器;
  • 异常事件处理
    • 用状态Stat表示
    • Stat=1AOK
    • Stat=2HLT
    • Stat=3ADR(即segmentation fault
    • Stat=4INS(非法指令)
    • 处理方式:让处理器停止执行

实验包根目录下有如下内容:

bash

.
├── archlab.pdf
├── Makefile
├── README
├── sim
├── simguide.pdf
└── sim.tar

其中archlab.pdf是实验指导手册(本文已翻译),Makefile中主要讲解的是如何提交实验(对于非CMU学生无用),sim.tar是实验包,simguide.pdf中描述的是如何使用文件中的simulator

sim中,有如下一些文件夹:

text

.
├── Makefile
├── misc
├── pipe
├── ptest
├── README
├── seq
└── y86-code

其中Makefile中描述的是如何构建所需要使用的Y86-64工具,使用make all构建,使用make clean清理。而README.md中写道,该学生包主要有如下几个部分:

bash

yas		Y86-64 assembler # 从Y86-64 汇编代码翻译成目标代码
yis		Y86-64 instruction (ISA) simulator # 模拟Y86-64指令运行
hcl2c		HCL to C translator # HCL 代码到 C 代码
hcl2v		HCL to Verilog translator # HCL 代码到 Verilog 代码
ssim		SEQ simulator # SEQ 处理器模拟器
ssim+		SEQ+ simulator # SEQ+ 处理器模拟器
psim		PIPE simulator # PIPE 处理器模拟器

模拟有2种模式:TTYGUITTY模式不如图形化界面方便。GUI界面需要安装Tcl/Tk工具才能运行。如果想要GUI模式,就把Makefile中的几个变量注释取消。

如果修改了Makefile文件,输入make clean; make再次构建即可。

此外,REAME.md介绍了几个文件夹中的内容:

bash

misc/ # 包含Y86-64汇编器,Y86-64指令模拟器,用于测试的isa.c文件,hcl2c和hcl2v
seq/ # SEQ 和 SEQ+ 模拟器,其中包含一些需要修改的HCL文件(修改SEQ)
pipe/ # PIPE 模拟器,其中包含一些需要修改的HCL文件(修改PIPE)
y86-code/ # 主要用于对新的模拟器进行基准测试
ptest/ # 主要用于对新的模拟器进行回归测试
verilog/ # 负责从HCL代码转化为Verilog代码

那么我们主要实验的部分就在misc/ seq/ pipe/三个文件夹中。

本阶段要求我们:

  • sim/misc目录下完成
  • 写并且模拟下面 3 个 Y86-64 程序,每个程序的行为都在examples.c
  • 把姓名放在程序开头的注释里
  • 如何测试程序
    • 用YAS汇编器编译
    • 用 YIS 模拟器运行
  • Y86-64 指令集架构对于函数的处理和 x86-64 相同
    • 传参方式相同
    • 寄存器使用方法相同
    • 用栈的方式相同
    • callee-saved register必须提前保存

三个函数的 C 语言版本如下:

c

/* linked list element */
typedef struct ELE {
    long val;
    struct ELE *next;
} *list_ptr;

/* sum_list - Sum the elements of a linked list */
long sum_list(list_ptr ls)
{
    long val = 0;
    while (ls) {
        val += ls->val;
        ls = ls->next;
    }
    return val;
}

/* rsum_list - Recursive version of sum_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 - Copy src to dest and return xor checksum of src */
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;
}

写一个Y86-64程序sum.ys,该程序迭代式的累加链表的元素。你的程序应该包括:

  • 设置栈结构
  • 调用函数
  • 停止

该函数应该是 C 语言程序sum_list的 Y83-64 形式。使用下面这个 3 元素的 list 来测试你的程序:

Y86-64

# Sample linked list
.align 8
ele1:
    .quad 0x00a
    .quad ele2
ele2:
    .quad 0x0b0
    .quad ele3
ele3:
    .quad 0xc00
    .quad 0

在开始写函数之前,我们需要在源文件中初始化好一些内容:

asm

# Execution begins at address 0
        .pos 0
        irmovq stack, %rsp
        call main
        halt
        
# Sample linked list
        .align 8
ele1:
        .quad 0x00a
        .quad ele2
ele2:
        .quad 0x0b0
        .quad ele3
ele3:
        .quad 0xc00
        .quad 0

main:
		# 此处放置main程序

sum_list:
		# 此处放置sum_list程序
		
		.pos 0x200
stack:

首先我们需要对源程序进行分析:

c

/* linked list element */
typedef struct ELE {
    long val;
    struct ELE *next;
} *list_ptr;

在这个结构体中,long是8个字节,struct ElE*是一个指针,占8个字节。这个结构体一共16个字节。那么在计算的过程中,如果传入的是结构体的指针,那么我们通过直接解引这个指针获取long val,然后通过list_ptr + 8来获取到next的地址,通过*(list_ptr + 8)来获取next指向的结点的地址。

我们要通过main调用sum_list,需要的步骤有:

  1. ele1的地址放在%rdi
  2. call sum_list
  3. ret

因此用Y86-64汇编可写为:

asm

main:
        # 传递参数
        irmovq ele1, %rdi
        call sum_list
        ret

然后就是观察sum_list函数:

c

/* sum_list - Sum the elements of a linked list */
long sum_list(list_ptr ls)
{
    long val = 0;
    while (ls) {
        val += ls->val;
        ls = ls->next;
    }
    return val;
}

这个函数获取一个list_ptr型的值,可以看到list_ptr型就是一个ElE型指针,那么这个值占8位。而且因为只有1个参数,因此它会存储在%rdi中。根据之前的学习,我们进行过程调用的步骤为:

  1. 保存之前的%rbp%rbx到栈上;
    • 本题不需要;
  2. 腾出一部分栈空间;
    • 本题val就是返回值,直接保存在%rax中;
  3. 将局部变量保存在在栈上;
    • 本题不需要;
  4. 保存调用者保存寄存器的值,到被调用者保存寄存器,这样可以在调用完成后从中取出原值;(这是一种保存调用者保存寄存器值的方法)
    • 本题不需要;
  5. 部分的值的地址需要作为参数传递,那么这些值必须压在栈上,以获取地址;
    • 本题不需要;
  6. 从第n到第7把参数依次压在栈上,第7个在栈顶;
    • 本题不需要;
  7. 把其他6个参数保存到寄存器中;
    • ls保存在%rdi中;
  8. call指令调用过程;
    • 本题不需要;
  9. 从栈中获取到值,以及从%rax中获取到返回值,进行计算;
    • val的值在%rax
  10. 恢复栈空间;
    • 本题不需要;
  11. 还原调用当前过程的%rbp%rbx
    • 本题不需要;
  12. 调用ret返回;

而现在问题来了,我们需要设计循环的结构,获取lsls->valls->next

  • 循环结构可采用jump to middle方法
  • ls = %rdils->val = *(ls)ls->next = *(ls + 8)

那么整体过程调用的内容为:

asm

sum_list:
	irmovq $0, %rax # val = 0 -> rax
	andq %rdi, %rdi
	jmp test
loop:
	mrmovq (%rdi), %r10
	addq %r10, %rax
	mrmovq 8(%rdi), %rsi
        rrmovq %rsi, %rdi
        andq %rdi, %rdi
test:
	jne loop
	ret

因此总的程序可写为:

Warning

注意:

  1. 一定要在循环测试前andq,否则测试通不过。
  2. stack后要加一个空行,否则编译通不过。

asm

# Execution begins at address 0
        .pos 0
        irmovq stack, %rsp
        call main
        halt
        
# Sample linked list
        .align 8
ele1:
        .quad 0x00a
        .quad ele2
ele2:
        .quad 0x0b0
        .quad ele3
ele3:
        .quad 0xc00
        .quad 0

main:
        # 传递参数
        irmovq ele1, %rdi
        call sum_list
        ret

sum_list:
	irmovq $0, %rax # val = 0 -> rax
	andq %rdi, %rdi
	jmp test
loop:
	mrmovq (%rdi), %r10
	addq %r10, %rax
	mrmovq 8(%rdi), %rdi
        andq %rdi, %rdi
test:
	jne loop
	ret

        .pos 0x200
stack:

写一个 Y86-64 程序,它可以递归的计算链表的元素的和,还是用上面的那个三个元素的链表进行测试。

rsum.ys和上述程序不一样的地方在于,它使用了递归的子过程,并且把ls->next作为子过程的参数传递。此外,它使用了if-else结构。需要注意的是,我们没有将局部变量压在栈上,而是使用%rsi来存储,但是每次迭代过程中都会产生新的值存在%rsi上,因此为了保存%rsi,我们需要把%rsi压在栈上,在调用返回后从栈中弹出,和%rax相加。

完整程序如下:

asm

# Execution begins at address 0
        .pos 0
        irmovq stack, %rsp
        call main
        halt
        
# Sample linked list
        .align 8
ele1:
        .quad 0x00a
        .quad ele2
ele2:
        .quad 0x0b0
        .quad ele3
ele3:
        .quad 0xc00
        .quad 0

main:
        # 传递参数
        irmovq ele1, %rdi
        call rsum_list
        ret

rsum_list:
        andq %rdi, %rdi
        je else
if:
        mrmovq (%rdi), %rsi
        pushq %rsi
        mrmovq 8(%rdi), %rdi
        call rsum_list
        popq %rsi
        addq %rsi, %rax
        jmp done
else:
        irmovq $0, %rax
done:
        ret

        .pos 0x200
stack:

实现一个叫copy.ys的函数,将一些字从内存的一块复制到另外一块(非重合区域),计算所有复制的字的校验和。程序应该包含如下部分:

  • 设置栈空间
  • 调用 function copy 代码块
  • 停止

使用下面的srcdest来测试:

Y86-64

.align 8
# Source block
src:
    .quad 0x00a
    .quad 0x0b0
    .quad 0xc00
# Destination block
dest:
    .quad 0x111
    .quad 0x222
    .quad 0x333

这个函数和之前的函数又不一样,我们来分析一下:

c

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;
}

这里我们需要通过main传入3个参数,srcdestlen。这三个参数都是8个字节,并且前两个是地址。因此我们通过%rdi %rsi %rdx三个寄存器传入。因此main部分的代码可以写为

asm

main:
        # 传递参数
        irmovq src, %rdi
        irmovq dest, %rsi
        irmovq $3, %rdx
        call copy_block
        ret

而在函数中,没有其他的过程调用,因此不需要保存什么变量到栈上。对于result,我们可以保存在%rax上(因为最后要返回),那么整体的代码可以翻译为:

asm

copy_block:
	irmovq $0, %rax # long result = 0
while:
	andq %rdx, %rdx 
	jle done
	addq $1, %rdi # src++
	mrmovq (%rdi), %r8 # %r8 = val = *src
	addq $1, %rsi
	rmmovq %r8,(%rsi)
	xorq %r8, %rax
	subq $1, %rdx
	jmp while
done:
	ret

我们进行汇编,发现汇编不成功,报错如下:

bash

../misc/yas copy.ys
Error on line 32: Expecting Register ID
Line 32, Byte 0x0087:   addq $1, %rdi # src++
Error on line 34: Expecting Register ID
Line 34, Byte 0x0093:   addq $1, %rsi
Error on line 37: Expecting Register ID
Line 37, Byte 0x00a1:   subq $1, %rdx
make: *** [Makefile:39: copy.yo] Error 1

回顾Y86-64指令集,我们想起来,不可以直接把常数加到寄存器上,因此我们需要用%r9寄存器来存储常数1,然后把寄存器相加。代码如下:

asm

copy_block:
	irmovq $0, %rax # long result = 0
    irmovq $1, %r9
while:
	andq %rdx, %rdx 
	jle done
	addq %r9, %rdi # src++
	mrmovq (%rdi), %r8 # %r8 = val = *src
	addq %r9, %rsi
	rmmovq %r8,(%rsi)
	xorq %r8, %rax
	subq %r9, %rdx
	jmp while
done:
	ret

这段代码虽然编译可以通过,但是我们发现还是有问题,数组一的内容压根就没有复制到数组二中去。经过仔细检查,我们发现问题出在src++dest++的处理上,这里srcdest是指向long类型的指针,但是我们在++的时候只加了1,按理来说应该加上8(sizeof(long))个单位。我们修改一下程序,新增一个变量%r10 = 8

asm

copy_block:
	irmovq $0, %rax # long result = 0
        irmovq $1, %r9
        irmovq $8, %r10
while:
	andq %rdx, %rdx 
	jle done
	addq %r10, %rdi # src++
	mrmovq (%rdi), %r8 # %r8 = val = *src
	addq %r10, %rsi
	rmmovq %r8,(%rsi)
	xorq %r8, %rax
	subq %r9, %rdx
	jmp while
done:
	ret
Tip

这里我们把数组第二项和第三项成功复制了,但是数组第一项没有。这是因为我们先进行了加,然后才进行的解引运算。但是其实我在网上查找资料的时候发现,C++中后增代码的运算级别是是高于*的,所以我才把程序写成这样。如果按照题目的要求,应该是先*然后再++,也就是说这段代码中的解引和递增的指令顺序应该反过来,才能完成程序的目标。

根据上述Tip,我们进行如下改动:

asm

copy_block:
	irmovq $0, %rax # long result = 0
        irmovq $1, %r9
        irmovq $8, %r10
while:
	andq %rdx, %rdx 
	jle done
	mrmovq (%rdi), %r8 # %r8 = val = *src
        addq %r10, %rdi # src++
	rmmovq %r8,(%rsi)
        addq %r10, %rsi
	xorq %r8, %rax
	subq %r9, %rdx
	jmp while
done:
	ret

成功!完整代码如下:

asm

# Execution begins at address 0
        .pos 0
        irmovq stack, %rsp
        call main
        halt
        
.align 8
# Source block
src:
    .quad 0x00a
    .quad 0x0b0
    .quad 0xc00
# Destination block
dest:
    .quad 0x111
    .quad 0x222
    .quad 0x333

main:
        # 传递参数
        irmovq src, %rdi
        irmovq dest, %rsi
        irmovq $3, %rdx
        call copy_block
        ret

copy_block:
	irmovq $0, %rax # long result = 0
        irmovq $1, %r9
        irmovq $8, %r10
while:
	andq %rdx, %rdx 
	jle done
	mrmovq (%rdi), %r8 # %r8 = val = *src
        addq %r10, %rdi # src++
	rmmovq %r8,(%rsi)
        addq %r10, %rsi
	xorq %r8, %rax
	subq %r9, %rdx
	jmp while
done:
	ret

        .pos 0x200
stack:
Warning

需要注意的点是:Makefile文件中tcl的版本可能需要更新,另外ssim.c中的matherr两行需要注释掉,不然会报错。 详情参考:

在本实验中,你需要在sim/seq目录下工作。这个部分我们的目的是扩展 SEQ 处理器,让其支持iaddq指令(书本 4.51 & 4.52 练习题内容),可以通过修改seq-full.hcl文件来实现,这里面包含了一些所需要用到的常数量。你的实现中在开头必须包含一个注释,在其中写上你的姓名和 ID,以及iaddq的执行过程。

text

1. 取指:icode:ifun <- M[PC] rA:rB <- M[PC+1] valC <- M[PC+2] valP <- PC+10
2. 译码:valB <- R[rB]
3. 执行:valE <- valB + valC
4. 访存:无
5. 写回:R[rB] <- valE
6. 更新PC:PC <- valP

在需要改动的信号中,直接将iaddq指令加到hcl列表中去即可。

需要改动的变量有:rA,rB,valC,valP

需要改动的信号有:needReg,needValC,instr_valid

Warning

这里需要srcB,因为我们是要将立即数与rB中的值相加,而不是赋值给rB

需要改动的变量有:srcB, dstE

Warning

一定要加set_cc,因为iaddq指令不属于原有的算术逻辑运算指令(IOPQ类)

需要改动的变量有:aluA, aluB, set_cc

无改动。

无改动。

当你修改完seq-full.hcl文件的时候,你需要构建一个新的 SEQ 处理器,并且测试它:

  • 构建一个新的模拟器

    bash

    make VERSION=full
  • 在一个简单的 Y86-64 程序上测试你的模拟器

    • 对于一个初始的测试,我们建议在 TTY 模式下运行一些简单的程序,例如asumi.yo,将其与模拟器的结果对比:

    bash

      ./ssim -t ../y86-code/asumi.yo
    • 如果测试失败,那么你应该在 GUI 模拟下单步调试模拟器,命令如下:

    bash

    ./ssim -g ../y86-code/asumi.yo
  • 使用基准测试来重新测试你的模拟器

    • 一旦你的模拟器可以正常执行小程序了,那么你可以利用y86-code中的基准程序进行测试,命令如下:

    bash

    cd ../y86-code; make testssim

    这个测试不包含新指令测试,只是检查新指令的执行会不会破坏原有的处理器状态,具体情况可以查看../y86-code/README文件

  • 进行回归测试

    • 一旦你可以正确运行基准程序,那么你应该运行一些../ptest中额外的回归测试,运行如下命令:

    bash

    cd ../ptest
    make SIM=../seq/ssim
    • 要测试iaddq指令,运行如下命令:

    bash

    cd ../ptest
    make SIM=../seq/ssim TFLAGS=-i
  • 其他 SEQ 模拟器的使用参考simguide.pdf

你会在sim/pipe目录下工作,ncopy函数将一个len个元素的数组从src复制到dst,返回其中正数的个数。图 3 显示了 Y86-64 版本的ncopy,文件pipe-full.hcl涵盖了 PIPE 的设计hcl代码,其中包括声明的常量IIADDQ

c

/*
* ncopy - copy src to dst, returning number of positive ints
* contained in src array.
* sim/pipe/ncopy.c
*/
word_t ncopy(word_t *src, word_t *dst, word_t len)
{
    word_t count = ;
    word_t val;

    while (len > ) {
        val = *src++;
        *dst++ = val;
        if (val > 0)
        count++;
        len--;
    }
    return count;
}

这一部分的任务是修改ncopy.yspipe-full.hcl,需要让ncopy.ys尽可能运行的快。

ncopy.ys如下:

hcl

##################################################################
# ncopy.ys - Copy a src block of len words to dst.
# Return the number of positive words (>0) contained in src.
#
# Include your name and ID here.
#
# Describe how and why you modified the baseline code.
#
##################################################################
# Do not modify this portion
# Function prologue.
# %rdi = src, %rsi = dst, %rdx = len
ncopy:

##################################################################
    # You can modify this portion
    # Loop header
    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:
##################################################################
# Do not modify the following section of code
# Function epilogue.
Done:
    ret
##################################################################
# Keep the following label at the end of your function
End:

最后需要上交两个文件:

  • pipe-full.hcl
  • ncopy.ys

每个文件前都需要有一个注释,其中包括:

  • 你的姓名和 ID
  • 你的代码解释,对每个场景描述你如何修改代码

这一部分的实验过程参考了一下:

首先我们需要将IIADDQ指令,像加到SEQ中那样加到PIPE的各个阶段中去,这里不多说。

下面我们对原始的ncopy.ys汇编代码进行测试,看看其效率j如何:

bash

Average CPE     15.18
Score   0.0/60.0

我们可以看到正确性测试可以通过,但是效率测试无法通过。因此我们需要提高效率。

接下来我们查看ncopy.ys的汇编代码,进行一些优化工作:

asm

	# Loop header
	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:

首先我们利用新添加的iaddq指令,把所有的经过寄存器的加法都改成iaddq

asm

	# Loop header
	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:
	iaddq $1, %rax  # count++
Npos:	
	iaddq $-1, %rdx     # len--
	iaddq $8, %rdi      # src++
	iaddq $8, %rsi		# dst++
	andq %rdx,%rdx		# len > 0?
	jg Loop			# if so, goto Loop:

怎么直接满分了?啊?!不是应该继续优化的吗?

你可以随意修改,但是有如下限制:

  • 你的ncopy.ys函数必须对任意大小的数组都适用,不可以对某个长度的数组进行硬编码。

  • 你的ncopy.ys必须在 YIS 上成功运行,成功的意思是成功的复制了数组,并且在%rax中返回了数组中正数的个数,

  • 你的ncopy汇编文件不能大于 1000 个字节,你可以使用如下命令检查:

    bash

    ./check-len.pl < ncopy.yo
  • 你的pipe-full.hcl文件实现必须通过../y86-code../ptest中的回归测试(不使用-i测试iaddq指令)

接下来你可以实现iaddq指令,修改时保证ncopy.ys的语义不改变。你可以阅读一下CSAPP 3e Section 5.8的循环展开。

为了测试你的实现,你需要构造一个调用你nocpy函数的驱动程序。我们已经为你提供了一个gen-driver.pl程序,该程序生成一个输入为任意大小数组的驱动程序,输入如下命令:

bash

make drivers

构建如下两个驱动程序:

  • sdriver.yo
    • 使用一个 4 个元素的小数组进行测试
    • 如果运行成功,最终%rax = 2
  • ldriver.yo
    • 使用一个大的 63 个元素的数组进行测试
    • 如果运行成功,最终%rax = 31

注意:

  • 每次你修改ncopy.ys,你都可以重新输入make drivers来重新构建驱动程序
  • 每次你修改pipe-full.hcl文件,你都可以输入make psim VERSION=full来重新构建模拟器
  • 如果你想同时重新构建两者,输入make VERSION=full
  • 要在 GUI 模式下测试你的小的 4 元素数组,输入./psim -g sdriver.yo
  • 要在一个更大的 63 个元素的数组下测试,输入。/psim -g ldriver.yo

一旦你的模拟器能在这两个数组下成功运行ncopy.ys,你需要执行下面这些额外的测试:

  • 在ISA模拟器上测试你的driver files,确保你的ncopy.ys函数能与YIS正常运行

    bash

    make drivers
    ../misc/yis sdriver.yo
  • 在不同大小的数组上测试ISA模拟器,perl脚本correctness.pl生成不同大小的driver files,它模拟它们,并且检查结果。他会生成一个显示最终结果的报告:

    bash

     ./correctness.pl
    • 如果在某个长度的数组上失败了, 你可以使用如下方法测试:

    bash

    unix> ./gen-driver.pl -f ncopy.ys -n K -rc > driver.ys
    unix> make driver.yo
    unix> ../misc/yis driver.yo
    • 返回值在%rax中,可能有如下几个值
      • 0xaaaa:所有测试通过
      • 0xbbbb:不正确
      • 0xcccc:ncopy函数的长度超过了1000字节
      • 0xdddd:部分源数据没有复制到目标地址
      • 0xeeee:目标地址前后的数据被改变了
  • 使用基准程序测试你的流水线模拟器,一旦你的模拟器能成功执行sdirver.ysldriver.ys,你应该使用../y86-code文件夹中的Y86-64基准程序:

    bash

    (cd ../y86-code; make testpsim)
    • 这会在基准程序上运行psim,然后将结果与YIS进行比较
  • 使用额外的回归测试来测试你的流水线模拟器。一旦你的模拟器能成功运行基准程序,那么你应该使用../ptest中的回归测试进行测试。比如你实现了iaddq指令,那么你可以执行如下命令进行测试:

    bash

    (cd ../ptest; make SIM=../pipe/psim TFLAGS=-i)
  • 在不同的块长度下测试你的程序,你可以使用与之前测试ISA模拟器相同的指令:

bash

./correctness.pl -p

整个实验 190 分,其中 Part A 30 分,Part B 60 分,Part C 100 分。

  • Part A 30分,其中每个Y86-64程序10分,其中每个程序都会被检测,包括对栈的处理和对寄存器的处理是否正确,以及和examples.c中的程序是否等效;
  • 如果grader 没有在sum.ysrsum.ys中发现任何错误,并且对应的sum listrsum list函数将0xcba保存在%rax中返回,被视为正确;
  • 如果grader没有在copy.ys中发现任何错误,并且copy block function将返回值0xcba保存在%rax中返回,并且复制了3个64位值0x00a,0x0b,0xc到从dest开始的24个字节中,并且没有破坏其他地方的值,被视为正确;

Part B 35分,其中10分给你对iaddq所需操作的描述,10分给y86代码通过基准regression tests,15分给iaddq通过ptest文件夹中的regression tests

Part C的分值是100,如果你在之前的测试中没有通过,那么你不会在这个阶段得到任何分数。

  • 20分给ncopy.yspipe-full.hcl中的头文件中的描述,以及代码实现的质量;

  • 60分给性能。也就是说ncopy需要成功在YIS下运行,并且pipe-full.hcl需要通过y86-codeptest文件夹下的所有测试。

  • 我们会使用CPE来测试ncopy的性能,也就是移动单位元素所花费的时钟周期。我们会使用多个不同长度的块来进行测试,使用如下命令完成

    bash

    ./benchmark.pl

    注意这个脚本不是用来测试正确性的,正确性应该用如下脚本进行测试:

    bash

    ./correctness.pl

    你的目标应该是达到平均小于9.00的CPE,评分标准如下:

    c

    S = 0; // c > 10.5
    20*(10.5-c) // 7.5 <= c <= 10.5
    60 // c < 7.50
    

    其中benchmark.plcorrectness.pl默认编译和测试ncopy.ys,但是还有如下几个可选项:

    bash

    -f # 测试其他文件
    -h # 输出所有命令行参数
  • sdriver.yoldriver.yo非常小,可以在GUI模式下调试;

  • 如果你在Unix的GUI模式下运行,你需要确保你已经初始化了DISPLAY环境变量

    bash

    setenv DISPLAY myhost.edu:0
  • 对于某些X servers,当你在GUI模式下运行psimssim时,Program Code窗口是个关闭图标。点击该图标就可以扩展窗口。

  • 在某些微软操作系统下的X servers,Memory Contents窗口不会自动缩放,你需要手动缩放。

  • 如果让psimssim模拟器去执行一个不是有效的Y86-64目标文件时,他们会报出段错误并终止执行。