[lab]csapp-archlab

archlab

该lab 要求我们在自制指令集 Y86-64 上进行编码, 并且提供一个简单的汇编器和模拟器实现.

由于是虚拟环境, 我们解压sim文件夹后要make 构建各个目标文件, 子目录如下

sim
  - misc    # 包含了 指令集(isa)/汇编器(yas)/模拟器(yis)
  - seq     # 顺序执行模式 hcl 的实现
  - pipe    # 流水线执行模式 hcl 的实现
  - ptest   # 测试脚本
  - y86-code # Y86-64 指令集的示例代码

PartA

要求将 example.c 中的三个函数用汇编实现, 要求实现带有main函数和设置栈顶, 我们首先从 y86-code 里拷贝出一份作为模版, 修改函数和数据即可.

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

sum_list 顺序遍历链表求和, rsum_list 递归遍历链表求和, copy_block 拷贝数组, 并计算异或和.

经过前面几个lab的洗礼, 还是很简单的, 大概从书上查阅一下(图4-2,4-3)支持的指令和意义就可以


# long sum_list(list_ptr ele)
# ele in %rdi
sum_list:
  irmovq $8, %r8    # Constant 8
	xorq %rax,%rax		# sum = 0
	andq %rdi,%rdi		# Set condition codes
	jmp  test
loop:
	mrmovq (%rdi),%r9	      # tmp = val
  addq %r9,%rax           # sum += tmp
	addq %r8,%rdi           # ele = ele++
  mrmovq (%rdi), %rdi     # ele = [ele]
	andq %rdi,%rdi		# Set condition codes
test:
	jne    loop             # Stop when 0
	ret


# long rsum_list(list_ptr ele)
# ele in %rdi
rsum_list:
  irmovq $8, %r8    # Constant 8
	xorq %rax,%rax		# sum = 0
	andq %rdi,%rdi		# Set condition codes
test:
	je    end             # Stop when 0
	pushq %rdi						# save ele
	addq %r8,%rdi           # ele = ele++
  mrmovq (%rdi), %rdi     # ele = [ele]
	call rsum_list					# rsum_list(ele)
	popq %rdi								# restore ele
	mrmovq (%rdi),%r9	      # tmp = val
  addq %r9,%rax           # sum += tmp
end:
	ret

# long copy_block(long *src, long* dest, long len)
# ele in %rdi
copy_block:
  irmovq $8, %r8    # Constant 8
	irmovq $1, %r10	  # Constant 1
	xorq %rax,%rax		# result = 0
	andq %rdx,%rdx		# Set condition codes
	jmp  test
loop:
	mrmovq (%rdi),%r9	      # tmp = *src
	addq %r8,%rdi           # src++
  xorq %r9,%rax           # sum ^= tmp
	rmmovq %r9,(%rsi)				# *dest = tmp
	addq %r8,%rsi						# dest++
	subq %r10,%rdx					# len--
test:
	jne    loop             # Stop when 0
	ret

PartB

在顺序流水线中实现 iaddq, 这个指令已经在isa.c/seq-full.hcl 有定义了, 我们只需要实现一遍 iaddq 在每个指令步骤的逻辑(图4-18)即可.

##	iaddq V, rB
## 
##	Fetch Stage
##		icode: ifunc <- m_1[PC]
##		rA:rB <- m_1[PC+1]
##		valC <- m_8[PC+2]
##	 	valP <- PC + 10
##	Decode Stage
##		valB <- R[rB]
## 	Execute Stage
##		valE <- valB + valC
## 	Memory Stage  
##		R[rB] <- valE 
## 	Program Counter Update
##		PC <- valP

在修改文件时可以对照IIRMOVQ指令, 因为IIADDQ与其十分相似, 只需要改动 aluB 和添加 set_cc 即可, alufun 因为已经默认时 ALUADD, 所以我们不需要改变它.

之后我们可以使用编译出的ssim(顺序执行指令模拟器)来测试我们的实现是否正确.

PartC

要对一个c函数对应的指令代码在流水线执行环境下手动优化.

流水线环境就是4.4中的内容, 我们还需要再实现一遍 iaddq, pipe 版多了状态传递的内容, 不过我们实现的思路可以照抄 seq 版.

然后我们来看需要优化的函数和他的初始实现

 /** ncopy - copy src to dst, returning number of positive ints
 * contained in src array.
 */  
word_t ncopy(word_t *src, word_t *dst, word_t len)
{ 
  word_t count = 0;
  word_t val;
  while (len > 0) {
    val = *src++;
    *dst++ = val;
    if (val > 0)
      count++;
    len--;
  }
  return count;
}

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

首先, 因为addq需要取寄存器两次, 我们将addq全部替换成iaddq.

在修改之后使用 make VERSION=full 编译, 然后使用 psim(并行执行指令模拟器)跑样例 sdriver.yo ldriver.yo 来测试我们的实现是否正确.

然后我们跑脚本 correctness.pl 来过大样例, 跑 benchmark.pl 来评分, 此时的分数应该还是0分, 还需要使用循环展开(5.8)和指令重排(5.9)来加速.

循环展开主要是并行的读取相邻元素, 降低循环的次数, 从而减少了循环变量计算和比较的次数.

指令重拍也是为了提升指令并行执行的数量, 比如我们想将rdi的内容拷贝到rsi中, 很自然的可以写 mrmovq (%rdi), %r10rmmovq %r10, (%rsi), 但这两条指令是有依赖的, 后面需要上一条指令将%10写入后才能开始. 我们在循环展开的条件下, 可以在中间再插入一条读取指令 mrmovq 8(%rdi), %11 有效避免了依赖造成的时钟浪费.

尽管有了思路, 但也不一定能写得出最优答案 , 在参照了blog后, 得到的代码如下

# You can modify this portion
	# Loop header
	xorq %rax,%rax		# count = 0;
	iaddq $-3, %rdx
	jle BeforeTail		# len <= 0? if so, goto Tail:
Extended4Loop:
	mrmovq (%rdi), %r9 	# read val1 from src...
	mrmovq 8(%rdi), %r10 	# read val2 from src...
	mrmovq 16(%rdi), %r11 	# read val3 from src...
	mrmovq 24(%rdi), %r12 	# read val4 from src...
	rmmovq %r9, (%rsi)	# store val to dst
	rmmovq %r10, 8(%rsi)	# store val to dst
	rmmovq %r11, 16(%rsi)	# store val to dst
	rmmovq %r12, 24(%rsi)	# store val to dst
	andq %r9, %r9		# val1 <= 0?
	jle Npos2		# if so, goto Npos:
	iaddq $1, %rax		# count++
Npos2:
	andq %r10, %r10		# val2 <= 0?
	jle Npos3		# if so, goto Npos:
	iaddq $1, %rax		# count++
Npos3:
	andq %r11, %r11		# val3 <= 0?
	jle Npos4		# if so, goto Npos:
	iaddq $1, %rax		# count++
Npos4:
	andq %r12, %r12		# val4 <= 0?
	jle Npos5		# if so, goto Npos:
	iaddq $1, %rax		# count++
Npos5:
	iaddq $32, %rdi		# src+=4
	iaddq $32, %rsi		# dst+=4
	iaddq $-4, %rdx		# len-=4
	jg Extended4Loop
BeforeTail:
	iaddq $3, %rdx
	jle Done		#  if so, goto Done:
	mrmovq (%rdi), %r9 	# read val from src...
	mrmovq 8(%rdi), %r10 	# read val from src...
	rmmovq %r9, (%rsi)	# store val to dst
	andq %r9, %r9		# val <= 0?
	jle Npos6		# if so, goto Npos:
	iaddq $1, %rax		# count++
Npos6:	
	iaddq $-1, %rdx		# len--
	jle Done			# if so, goto Done:
	rmmovq %r10, 8(%rsi)	# store val to dst
	andq %r10, %r10		# val <= 0?
	jle Npos7		# if so, goto Npos:
	iaddq $1, %rax		# count++
Npos7:
	iaddq $-1, %rdx		# len--
	jle Done			# if so, goto Done:
	mrmovq 16(%rdi), %r11 	# read val from src...
	rmmovq %r11, 16(%rsi)	# store val to dst
	andq %r11, %r11		# val <= 0?
	jle Done		# if so, goto Npos:
	iaddq $1, %rax		# count++

结果和博客一样都是48.6, 自己在实现的过程中忽略了movq可以变址读取, 没有利用到循环展开的优势, 且计算循环展开的剩余部分比较复杂, 直接-3, 再加3, 这样可以保证进入结束部分时len就是原来 len 对4的余数, 能比直接计算余数减少2-3个指令.

What‘s more

这个lab提供的文件给出了一个完成的指令集设计和实现, 也是非常好的学习示例.

我们来看PartA, 利用Lex&Yaccyas,hcl2c相关的部分和实现.

Lex&Yacc 是一组生成软件, 它接受自定义的语法规则, 生成对应规则的解析器. Flex&Bison 是Linux环境中对他们的一组实现, 更多知识可以参考这个, 简单来说 Lex就是分词器, Yacc则接受分词, 根据自定义的语法规则生成解析代码(就是我们的编译器).

我们来列举下misc中需要关注的文件

Makefile # 编译 target 的命令


isa.h/c

yas.h/c
yas-grammar.lex # 用于生成yas

yis.h/c         # yis

node.h/c        # 编译节点定义
outgen.h/c      # 打印输出的代码
hcl.lex/y       # hcl

isa 里有

  • 寄存器定义
  • 指令编码
  • 指令描述表
  • 内存操作
  • 寄存器操作
  • ALU操作 : 计算/更新和获取状态字
  • 指令状态 stat_t
  • 虚拟机状态: state_rec, 指令跳转, 指令执行

首先是 yas, 它是Y86的汇编器, 负责从汇编代码到机器指令(ys->yo)这一步, 依赖了yas-grammar , isa

yas-grammar.o: yas-grammar.c
	$(CC) $(LCFLAGS) -c yas-grammar.c

yas-grammar.c: yas-grammar.lex
	$(LEX) yas-grammar.lex
	mv lex.yy.c yas-grammar.c

isa.o: isa.c isa.h
	$(CC) $(CFLAGS) -c isa.c

yas.o: yas.c yas.h isa.h
	$(CC) $(CFLAGS) -c yas.c

yas: yas.o yas-grammar.o isa.o
	$(CC) $(CFLAGS) yas-grammar.o yas.o isa.o ${LEXLIB} -o yas

yas.h 内容如下,

void save_line(char *);
void finish_line();
void add_reg(char *);
void add_ident(char *);
void add_instr(char *);
void add_punct(char);
void add_num(long long);
void fail(char *msg);
unsigned long long atollh(const char *);


/* Current line number */
int lineno;

yas-grammar.lex 内容如下,

/* Grammar for Y86-64 Assembler */
 #include "yas.h"

Instr         rrmovq|cmovle|cmovl|cmove|cmovne|cmovge|cmovg|rmmovq|mrmovq|irmovq|addq|subq|andq|xorq|jmp|jle|jl|je|jne|jge|jg|call|ret|pushq|popq|"."byte|"."word|"."long|"."quad|"."pos|"."align|halt|nop|iaddq
Letter        [a-zA-Z]
Digit         [0-9]
Ident         {Letter}({Letter}|{Digit}|_)*
Hex           [0-9a-fA-F]
Blank         [ \t]
Newline       [\n\r]
Return        [\r]
Char          [^\n\r]
Reg           %rax|%rcx|%rdx|%rbx|%rsi|%rdi|%rsp|%rbp|%r8|%r9|%r10|%r11|%r12|%r13|%r14

%x ERR COM
%%

^{Char}*{Return}*{Newline}      { save_line(yytext); REJECT;} /* Snarf input line */
#{Char}*{Return}*{Newline}      {finish_line(); lineno++;}
"//"{Char}*{Return}*{Newline}     {finish_line(); lineno++;}
"/*"{Char}*{Return}*{Newline}   {finish_line(); lineno++;}
{Blank}*{Return}*{Newline}      {finish_line(); lineno++;}

{Blank}+          ;
"$"+              ;
{Instr}           add_instr(yytext);
{Reg}             add_reg(yytext);
[-]?{Digit}+      add_num(atoll(yytext));
"0"[xX]{Hex}+     add_num(atollh(yytext));
[():,]            add_punct(*yytext);
{Ident}           add_ident(yytext);
{Char}            {; BEGIN ERR;}
<ERR>{Char}*{Newline} {fail("Invalid line"); lineno++; BEGIN 0;}
%%

unsigned int atoh(const char *s)
{
    return(strtoul(s, NULL, 16));
}

我们可以看出yas的编译逻辑是按行编译, 解析出token就将他们加入到当前行中, 当前行结束时调用finish_line 产生一条指令.

hcl略微复杂, 它通过.lex文件先生成token流, 但是交给 .y 文件定义的语法规则来处理, 从而能处理更复杂的hcl语法, 将他们生成c/Verilog文件.

posted @ 2022-03-29 19:59  新新人類  阅读(262)  评论(0编辑  收藏  举报