CSAPP-Lab04 Architecture Lab 深入解析
窮且益堅,不墜青雲之志。
實驗概覽
Arch Lab 實驗分為三部分。在 A 部分中,需要我們寫一些簡單的Y86-64
程式,從而熟悉Y86-64
工具的使用;在 B 部分中,我們要用一個新的指令來擴充套件SEQ
;C 部分是本實驗的核心,我們要通過理解流水線的過程以及利用新的指令來優化程式。
實驗材料中有一個archlab.pdf
,按照文件一步步往下走就可以了。make
時,可能會缺少相關依賴,安裝如下軟體即可
sudo apt install tcl tcl-dev tk tk-dev
sudo apt install flex
sudo apt install bison
Part A
在這部分,要用Y86-64
examples.c
中的三個函式。這三個函式都是與連結串列有關的操作,連結串列結點定義如下
/* linked list element */
typedef struct ELE {
long val;
struct ELE *next;
} *list_ptr;
在編寫彙編程式碼之前,我們先回顧一下Y86-64
的指令集:
# movq i-->r: 從立即數到暫存器... irmovq, rrmovq, mrmovq, rmmovq # Opq addq, subq, andq, xorq # 跳轉 jXX jmp, jle, jl, je, jne, jge, jg # 條件傳送 cmovXX cmovle, cmovl, cmove, cmovne, cmovge, cmovg call, ret pushq, popq # 停止指令的執行 halt # 暫存器 %rax, %rcx, %rdx %rbx, %rsp, %rbp %rsi, %rdi, %r8 %r9, %r10, %r11 %r12, %r13, %r14
sum_list
/* 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;
}
本題就是一個連結串列求和,非常簡單。但要注意,這裡不僅要寫出函式段,還應該寫出測試的程式碼段。直接給出轉換後的彙編程式碼:
# sum_list - Sum the elements of a linked list # author: Deconx # Execution begins at address 0 .pos 0 irmovq stack, %rsp # Set up stack pointer call main # Execute main program halt # Terminate program # 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 # long sum_list(list_ptr ls) # start in %rdi sum_list: irmovq $0, %rax jmp test loop: mrmovq (%rdi), %rsi addq %rsi, %rax mrmovq 8(%rdi), %rdi test: andq %rdi, %rdi jne loop ret # Stack starts here and grows to lower addresses .pos 0x200 stack:
注意,應在stack
下方空一行,否則彙編器會報錯,報錯原因我也不清楚。
利用實驗檔案中給的YAS
彙編器進行彙編,YIS
指令集模擬器執行測試
./yas sum.ys
./yis sum.yo
得到結果
返回值%rax=0xcba=0x00a+0x0b0+0xc00
,結果正確!
rsum_list
/* 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;
}
}
這是連結串列求和的遞迴實現,按照C語言程式碼的過程模擬即可,思路非常清晰,可以參考我的註釋
# /* rsum_list - Recursive version of sum_list */
# author: Deconx
# Execution begins at address 0
.pos 0
irmovq stack, %rsp # Set up stack pointer
call main # Execute main program
halt # Terminate program
# 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
# long sum_list(list_ptr ls)
# start in %rdi
rsum_list:
andq %rdi, %rdi
je return # if(!ls)
mrmovq (%rdi), %rbx # val = ls->val
mrmovq 8(%rdi), %rdi # ls = ls->next
pushq %rbx
call rsum_list # rsum_list(ls->next)
popq %rbx
addq %rbx, %rax # val + rest
ret
return:
irmovq $0, %rax
ret
# Stack starts here and grows to lower addresses
.pos 0x200
stack:
測試
結果正確!
copy_block
/* 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;
}
陣列賦值操作,返回值為原陣列各項的按位異或
這段程式碼的架構與書上圖 4-7的例子完全相同,包括常數的處理,迴圈的設定技巧,退出迴圈的判斷... 照貓畫虎即可,當然,我也在後面附上了註釋
/* copy_block - Copy src to dest and return xor checksum of src */
# author: Deconx
# Execution begins at address 0
.pos 0
irmovq stack, %rsp # Set up stack pointer
call main # Execute main program
halt # Terminate program
# Sample
.align 8
# Source block
src:
.quad 0x00a
.quad 0x0b0
.quad 0xc00
# Destination block
dest:
.quad 0x111
.quad 0x222
.quad 0x333
main:
irmovq src, %rdi # src
irmovq dest, %rsi # dest
irmovq $3, %rdx # len
call copy_block
ret
# long copy_block(long *src, long *dest, long len)
# src in %rdi
# dest in %rsi
# len in %rdx
copy_block:
irmovq $8, %r8
irmovq $1, %r9
irmovq $0, %rax
andq %rdx, %rdx
jmp test
loop:
mrmovq (%rdi), %r10 # val = *src1
addq %r8, %rdi # src++
rmmovq %r10, (%rsi) # *dest = val
addq %r8, %rsi # dest++
xorq %r10, %rax # result ^= val
subq %r9, %rdx # len--. Set CC
test:
jne loop # Stop when 0
ret
# Stack starts here and grows to lower addresses
.pos 0x200
stack:
編譯執行一下
結果完全正確
Part B
Part B 整合了第 4 章的 homework - 4.51, 4.52。就是實現iaddq
指令,將立即數與暫存器相加。可以參考irmovq
和OPq
指令的計算。在開始之前,我們還是先回顧一下處理一條指令的各個階段吧!
回顧:指令處理框架
-
取址:根據 PC 的值從記憶體中讀取指令位元組
- 指令指示符位元組的兩個四位部分,為
icode:ifun
- 暫存器指示符位元組,為
rA
,rB
- 8位元組常數字,為
valC
- 計算下一條指令地址,為
valP
- 指令指示符位元組的兩個四位部分,為
-
譯碼:從暫存器讀入最多兩個運算元
- 由
rA
,rB
指明的暫存器,讀為valA
,valB
- 對於指令
popq
,pushq
,call
,ret
也可能從%rsp
中讀
- 由
-
執行:根據
ifun
計算,或計算記憶體引用的有效地址,或增加或減少棧指標- 對上述三者之一進行的操作得到的值為
valE
- 如果是計算,則設定條件碼
- 對於條件傳送指令,檢驗條件碼和傳送條件,並據此更新目標暫存器
- 對於跳轉指令,決定是否選擇分支
- 對上述三者之一進行的操作得到的值為
-
訪存:顧名思義
- 可能是將資料寫入記憶體
- 若是從記憶體中讀出資料,則讀出的值為
valM
- 寫回:最多寫兩個結果到暫存器
- 更新 PC:將 PC 設定成下一條指令的地址
iaddq
指令執行過程
iaddq
的執行與Opq
非常相似,後者需要取出rA
與rB
分別指示的暫存器進行運算後再寫回rB
指示的暫存器。而前者與後者唯一的區別就是,不需要從rA
中取數,直接立即數計算即可。
指令為:iaddq V, rB
取指:
icode:ifun <- M_1[PC]
rA:rB <- M_1[PC+1]
valC <- M_8[PC+2]
valP <- PC+10
譯碼:
valB <- R[rB]
執行:
valE <- valB + valC
Set CC
訪存:
寫回:
R[rB] <- valE
更新PC:
PC <- valP
修改HCL
程式碼
接下來要在seq-full.hcl
檔案中修改程式碼。由於iaddq
的操作與OPq
和irmovq
類似,比較取巧的做法是,搜尋有這兩個指令的描述塊進行修改即可。本著學習的目的,我們分階段對所有訊號逐個分析
取指階段
instr_valid
:判斷指令是否合法,當然應該加上。修改後為
bool instr_valid = icode in
{ INOP, IHALT, IRRMOVQ, IIRMOVQ, IRMMOVQ, IMRMOVQ,
IOPQ, IJXX, ICALL, IRET, IPUSHQ, IPOPQ, IIADDQ };
need_regids
:判斷指令是否包括暫存器指示符位元組,當然也應該加上
bool need_regids =
icode in { IRRMOVQ, IOPQ, IPUSHQ, IPOPQ,
IIRMOVQ, IRMMOVQ, IMRMOVQ, IIADDQ };
need_valC
:判斷指令是否包括常數字,還是要加上
bool need_valC =
icode in { IIRMOVQ, IRMMOVQ, IMRMOVQ, IJXX, ICALL, IIADDQ };
譯碼和寫回階段
srcB
:賦為產生valB
的暫存器。譯碼階段要從rA
, rB
指明的暫存器讀為 valA
, valB
,而iaddq
有一個rB
,於是有以下修改
word srcB = [
icode in { IOPQ, IRMMOVQ, IMRMOVQ, IIADDQ } : rB;
icode in { IPUSHQ, IPOPQ, ICALL, IRET } : RRSP;
1 : RNONE; # Don't need register
];
dst_E
:表明寫埠 E 的目的暫存器,計算出來的值valE
將放在那裡。最終結果要存放在rB
中,所以要修改
word dstE = [
icode in { IRRMOVQ } && Cnd : rB;
icode in { IIRMOVQ, IOPQ, IIADDQ } : rB;
icode in { IPUSHQ, IPOPQ, ICALL, IRET } : RRSP;
1 : RNONE; # Don't write any register
];
執行階段
執行階段ALU
要對aluA
和aluB
進行計算,計算格式為:aluB OP aluA
。所以aluaA
可以是valA
和valC
或者+-8
,aluaB
只能是valB
。而iaddq
執行階段進行的運算是valB + valC
,於是可知修改
## Select input A to ALU
word aluA = [
icode in { IRRMOVQ, IOPQ } : valA;
icode in { IIRMOVQ, IRMMOVQ, IMRMOVQ, IIADDQ } : valC;
icode in { ICALL, IPUSHQ } : -8;
icode in { IRET, IPOPQ } : 8;
# Other instructions don't need ALU
];
## Select input B to ALU
word aluB = [
icode in { IRMMOVQ, IMRMOVQ, IOPQ, ICALL,
IPUSHQ, IRET, IPOPQ, IIADDQ } : valB;
icode in { IRRMOVQ, IIRMOVQ } : 0;
# Other instructions don't need ALU
];
set_cc
:判斷是否應該更新條件碼暫存器,這裡應該加上
bool set_cc = icode in { IOPQ, IIADDQ };
訪存階段
iaddq
沒有訪存階段,無需修改
更新PC階段
iaddq
不涉及轉移等操作,也無需修改
測試SEQ
編譯失敗處理辦法
編譯ssim
的時候出現了很多問題:
提示不存在tk.h
這個標頭檔案,這是由於實驗檔案太老。把Makefile
修改一下。第 20 行改為
TKINC=-isystem /usr/include/tcl8.6
第 26 行改為
CFLAGS=-Wall -O2 -DUSE_INTERP_RESULT
但是接下來還是報錯了
/usr/bin/ld: /tmp/ccKTMI04.o:(.data.rel+0x0): undefined reference to `matherr'
collect2: error: ld returned 1 exit status
make: *** [Makefile:44: ssim] Error 1
這是因為較新版本glibc
棄用了這部分內容
解決辦法是註釋掉 /sim/pipe/psim.c 806、807 line
和 /sim/seq/ssim.c 844、845 line
。即:有原始碼中有matherr
的一行和它的下一行
接下來就能編譯成功了!雖然會有很多 Warning
測試
第一輪測試
執行一個簡單的Y86-64
程式,並將結果ISA
模擬器的結果進行比對,輸出如下
> ./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
成功!
標準測試
執行一個標準檢查程式
> cd ../y86-code; make testssim
../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
全部都是 Succeeds
迴歸測試
測試除iaddq
的所有指令
專門測試iaddq
指令
於是,我們就通過了實驗材料中的所有測試用例!
Part C
Part C 在sim/pipe
中進行。PIPE 是使用了轉發技術的流水線化的Y86-64
處理器。它相比 Part B 增加了流水線暫存器和流水線控制邏輯。
在本部分中,我們要通過修改pipe-full.hcl
和ncopy.ys
來優化程式,通過程式的效率,也就是 CPE 來計算我們的分數,分數由下述公式算出
首先,iaddq
是一個非常好的指令,它可以把兩步簡化為一步,所以我們先修改pipe-full.hcl
,增加iaddq
指令,修改參考 Part B 即可。穩妥起見,修改後還是應該測試一下這個模擬器,Makefile
參考 Part B 部分進行同樣的修改後編譯。然後執行以下命令進行測試:
./psim -t ../y86-code/asumi.yo
cd ../ptest; make SIM=../pipe/psim
cd ../ptest; make SIM=../pipe/psim TFLAGS=-i
當所有測試都顯示 Succeed 後,就可以真正開始本部分的重頭戲了!
ncopy
函式將一個長度為len
的整型陣列src
複製到一個不重疊的陣列dst
,並返回src
中正數的個數。C 語言程式碼如下
/*
* 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:
先分別執行以下命令,對原始程式碼測試一波 CPE
./correctness.pl
./benchmark.pl
得
Average CPE 15.18
Score 0.0/60.0
利用iaddq
首先能夠直觀看到,為了len--/src++/dst++
等操作,對%rdi
進行了不少次賦值操作,這些都可以用我們新增的iaddq
指令替代。
替代後代碼為
# 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:
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:
測試 CPE
Average CPE 12.70
Score 0.0/60.0
雖然分數還是0,但已經有了不少提升
迴圈展開
根據文件的提示,可以試試迴圈展開進行優化。 迴圈展開通過增加每次迭代計算的元素的數量,減少迴圈的迭代次數。這樣做對效率提升有什麼作用呢?
- 減少了索引計算的次數
- 減少了條件分支的判斷次數
那麼展開幾路效率最高呢?我從5路展開開始分別進行了測試
5路:
Average CPE 9.61
Score 17.8/60.0
6路:
Average CPE 9.58
Score 18.3/60.0
7路:
Average CPE 9.59
Score 18.2/60.0
8路:
Average CPE 9.62
Score 17.5/60.0
所以,我選擇進行6路展開
# Loop header
andq %rdx,%rdx # len <= 0?
jmp test
Loop:
mrmovq (%rdi),%r8
rmmovq %r8,(%rsi)
andq %r8,%r8
jle Loop1
iaddq $1,%rax
Loop1:
mrmovq 8(%rdi),%r8
rmmovq %r8,8(%rsi)
andq %r8,%r8
jle Loop2
iaddq $1,%rax
Loop2:
mrmovq 16(%rdi),%r8
rmmovq %r8,16(%rsi)
andq %r8,%r8
jle Loop3
iaddq $1,%rax
Loop3:
mrmovq 24(%rdi),%r8
rmmovq %r8,24(%rsi)
andq %r8,%r8
jle Loop4
iaddq $1,%rax
Loop4:
mrmovq 32(%rdi),%r8
rmmovq %r8,32(%rsi)
andq %r8,%r8
jle Loop5
iaddq $1,%rax
Loop5:
mrmovq 40(%rdi),%r8
rmmovq %r8,40(%rsi)
iaddq $48,%rdi
iaddq $48,%rsi
andq %r8,%r8
jle test
iaddq $1,%rax
test:
iaddq $-6, %rdx # 先減,判斷夠不夠6個
jge Loop # 6路展開
iaddq $-8,%rdi
iaddq $-8,%rsi
iaddq $6, %rdx
jmp test2 #剩下的
Lore:
mrmovq (%rdi),%r8
rmmovq %r8,(%rsi)
andq %r8,%r8
jle test2
iaddq $1,%rax
test2:
iaddq $8,%rdi
iaddq $8,%rsi
iaddq $-1, %rdx
jge Lore
程式碼邏輯非常簡單:每次迴圈都對6個數進行復制,每次複製就設定一個條件語句判斷返回時是否加1,對於剩下的資料每次迴圈只對1個數進行復制。
為了方便分析,我把極端的幾個例子的情況列下來:
ncopy
0 26
1 35 35.00
2 47 23.50
3 56 18.67
4 68 17.00
5 77 15.40
6 69 11.50
7 78 11.14
8 90 11.25
9 99 11.00
10 111 11.10
11 120 10.91
12 112 9.33
13 121 9.31
14 133 9.50
15 142 9.47
16 154 9.62
17 163 9.59
18 155 8.61
...
50 391 7.82
51 400 7.84
52 412 7.92
53 421 7.94
54 413 7.65
55 422 7.67
56 434 7.75
57 443 7.77
58 455 7.84
59 464 7.86
60 456 7.60
61 465 7.62
62 477 7.69
63 486 7.71
64 498 7.78
Average CPE 9.58
Score 18.3/60.0
觀察上表,對於小資料而言, CPE 的值非常大,後續可以考慮對小資料進行優化。我們先優化剩餘資料的處理,對他們繼續進行迴圈展開。
剩餘資料處理
對於剩餘資料,我選擇3路迴圈展開。前面的6路與上面程式碼一樣,我就不再貼出來了
# Loop header
andq %rdx,%rdx # len <= 0?
jmp test
Loop:...
Loop1:...
...
Loop4:...
Loop5:...
test:
iaddq $-6, %rdx # 先減,判斷夠不夠6個
jge Loop # 6路展開
iaddq $6, %rdx
jmp test2 #剩下的
L:
mrmovq (%rdi),%r8
rmmovq %r8,(%rsi)
andq %r8,%r8
jle L1
iaddq $1,%rax
L1:
mrmovq 8(%rdi),%r8
rmmovq %r8,8(%rsi)
andq %r8,%r8
jle L2
iaddq $1,%rax
L2:
mrmovq 16(%rdi),%r8
rmmovq %r8,16(%rsi)
iaddq $24,%rdi
iaddq $24,%rsi
andq %r8,%r8
jle test2
iaddq $1,%rax
test2:
iaddq $-3, %rdx # 先減,判斷夠不夠3個
jge L
iaddq $2, %rdx # -1則不剩了,直接Done,0 剩一個, 1剩2個
je R0
jl Done
mrmovq (%rdi),%r8
rmmovq %r8,(%rsi)
andq %r8,%r8
jle R2
iaddq $1,%rax
R2:
mrmovq 8(%rdi),%r8
rmmovq %r8,8(%rsi)
andq %r8,%r8
jle Done
iaddq $1,%rax
jmp Done
R0:
mrmovq (%rdi),%r8
rmmovq %r8,(%rsi)
andq %r8,%r8
jle Done
iaddq $1,%rax
注意對於3路展開的特殊處理。看第38、39行,通過直接判斷剩餘資料的數量減少一次條件判斷
CPE 值為
Average CPE 9.07
Score 28.5/60.0
提升了很多,但是依然連一般的分數都還沒拿到...
消除氣泡
注意,程式多次使用了下面的操作:
mrmovq (%rdi), %r8
rmmovq %r8, (%rsi)
Y86-64
處理器的流水線有 F(取指)、D(譯碼)、E(執行)、M(訪存)、W(寫回) 五個階段,D 階段才讀取暫存器,M 階段才讀取對應記憶體值,
即使使用轉發來避免資料冒險,這其中也至少會有一個氣泡。像這樣
mrmovq (%rdi), %r8
bubble
rmmovq %r8, (%rsi)
一個優化辦法是,多取一個暫存器,連續進行兩次資料複製。
mrmovq (%rdi), %r8
mrmovq 8(%rdi), %r9
rmmovq %r8, (%rsi)
rmmovq %r9, 8(%rsi)
像這樣,對%r8
和%r9
進行讀入和讀出的操作之間都隔著一條其他指令,就不會有氣泡產生了。程式碼如下:
# Loop header
andq %rdx,%rdx # len <= 0?
jmp test
Loop:
mrmovq (%rdi),%r8
mrmovq 8(%rdi),%r9
andq %r8,%r8
rmmovq %r8,(%rsi)
rmmovq %r9,8(%rsi)
jle Loop1
iaddq $1,%rax
Loop1:
andq %r9,%r9
jle Loop2
iaddq $1,%rax
Loop2:
mrmovq 16(%rdi),%r8
mrmovq 24(%rdi),%r9
andq %r8,%r8
rmmovq %r8,16(%rsi)
rmmovq %r9,24(%rsi)
jle Loop3
iaddq $1,%rax
Loop3:
andq %r9,%r9
jle Loop4
iaddq $1,%rax
Loop4:
mrmovq 32(%rdi),%r8
mrmovq 40(%rdi),%r9
andq %r8,%r8
rmmovq %r8,32(%rsi)
rmmovq %r9,40(%rsi)
jle Loop5
iaddq $1,%rax
Loop5:
iaddq $48,%rdi
iaddq $48,%rsi
andq %r9,%r9
jle test
iaddq $1,%rax
test:
iaddq $-6, %rdx # 先減,判斷夠不夠6個
jge Loop # 6路展開
iaddq $6, %rdx
jmp test2 #剩下的
L:
mrmovq (%rdi),%r8
andq %r8,%r8
rmmovq %r8,(%rsi)
jle L1
iaddq $1,%rax
L1:
mrmovq 8(%rdi),%r8
andq %r8,%r8
rmmovq %r8,8(%rsi)
jle L2
iaddq $1,%rax
L2:
mrmovq 16(%rdi),%r8
iaddq $24,%rdi
rmmovq %r8,16(%rsi)
iaddq $24,%rsi
andq %r8,%r8
jle test2
iaddq $1,%rax
test2:
iaddq $-3, %rdx # 先減,判斷夠不夠3個
jge L
iaddq $2, %rdx # -1則不剩了,直接Done,0 剩一個, 1剩2個
je R0
jl Done
mrmovq (%rdi),%r8
mrmovq 8(%rdi),%r9
rmmovq %r8,(%rsi)
rmmovq %r9,8(%rsi)
andq %r8,%r8
jle R2
iaddq $1,%rax
R2:
andq %r9,%r9
jle Done
iaddq $1,%rax
jmp Done
R0:
mrmovq (%rdi),%r8
andq %r8,%r8
rmmovq %r8,(%rsi)
jle Done
iaddq $1,%rax
注意,只有rmmovq
不改變條件暫存器的值,所以我們也可以把andq
插進中間來消除氣泡。
CPE 值為
Average CPE 8.16
Score 46.9/60.0
這一步的提升是巨大的!我的分數終於像點樣子了!
進一步優化
這裡先留個坑。
暫且截圖記錄我目前為止的最高成就:
執行正確:
分數為:46.8
總結
- 讀 CSAPP 第 4 章時,我理解得很不通透,部分內容甚至有些迷糊。而做完了本實驗,通過親自設計指令,親自模擬流水線的工作過程並思考如何優化,我對處理器體系結構有了更深的感悟,有一種瞭然於胸的感覺。
- CMU 的這兩位大神老師 Randal E. Bryant 和 David R. O'Hallaron 簡直令我佩服得五體投地。我本以為他們只是從理論層面上將第 4 章的處理器指令,流水線如何設計等等教授給我們。沒想到,他們竟然真正設計實現了這樣一套完整的
Y86-64
模擬器、測試工具供我們學習。本實驗尤其是 Part C 每優化一次就能立即看到自己的分數,這猶如遊戲闖關一般的體驗令我著迷。這一切要歸功於兩位老師細緻的設計,希望有生之年能見他們一次! - 作為一個完美主義者,我在 Part C 部分卻沒有拿到滿分,這簡直是無法忍受的。但是我著實學業繁忙,不能在這個實驗耗費太多時間,只能暫且擱置,暑假回來繼續幹它!
- 本實驗耗時 3 天,約 17 小時