1. 程式人生 > 其它 >[lab]csapp-archlab

[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檔案.