[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), %r10
和 rmmovq %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&Yacc
對yas,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檔案.