CSAPP-Lab04 Architecture Lab 深入解析

Deconx發表於2022-03-13

窮且益堅,不墜青雲之志。

實驗概覽

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

得到結果

image-20220312124402451

返回值%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: 

測試

image-20220312143805214

結果正確!

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: 

編譯執行一下

image-20220312155107577

結果完全正確

Part B

Part B 整合了第 4 章的 homework - 4.51, 4.52。就是實現iaddq指令,將立即數與暫存器相加。可以參考irmovqOPq指令的計算。在開始之前,我們還是先回顧一下處理一條指令的各個階段吧!

回顧:指令處理框架

  • 取址:根據 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非常相似,後者需要取出rArB分別指示的暫存器進行運算後再寫回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的操作與OPqirmovq類似,比較取巧的做法是,搜尋有這兩個指令的描述塊進行修改即可。本著學習的目的,我們分階段對所有訊號逐個分析

取指階段

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要對aluAaluB進行計算,計算格式為:aluB OP aluA。所以aluaA可以是valAvalC或者+-8aluaB只能是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的時候出現了很多問題:

image-20220312222140881

提示不存在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的所有指令

image-20220312224855040

專門測試iaddq指令

image-20220312225008116

於是,我們就通過了實驗材料中的所有測試用例!

Part C

Part C 在sim/pipe中進行。PIPE 是使用了轉發技術的流水線化的Y86-64處理器。它相比 Part B 增加了流水線暫存器和流水線控制邏輯。

在本部分中,我們要通過修改pipe-full.hclncopy.ys來優化程式,通過程式的效率,也就是 CPE 來計算我們的分數,分數由下述公式算出

\[S=\begin{cases} 0, c>10.5\\ 20\cdot \left( 10.5-c \right) , 7.50\leqslant c\leqslant 10.50\\ 60, c<7.50\\ \end{cases} \]

首先,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

這一步的提升是巨大的!我的分數終於像點樣子了!

進一步優化

這裡先留個坑。

暫且截圖記錄我目前為止的最高成就:

執行正確:

image-20220313213326792

分數為:46.8

image-20220313213349365

總結

  • 讀 CSAPP 第 4 章時,我理解得很不通透,部分內容甚至有些迷糊。而做完了本實驗,通過親自設計指令,親自模擬流水線的工作過程並思考如何優化,我對處理器體系結構有了更深的感悟,有一種瞭然於胸的感覺。
  • CMU 的這兩位大神老師 Randal E. Bryant 和 David R. O'Hallaron 簡直令我佩服得五體投地。我本以為他們只是從理論層面上將第 4 章的處理器指令,流水線如何設計等等教授給我們。沒想到,他們竟然真正設計實現了這樣一套完整的Y86-64模擬器、測試工具供我們學習。本實驗尤其是 Part C 每優化一次就能立即看到自己的分數,這猶如遊戲闖關一般的體驗令我著迷。這一切要歸功於兩位老師細緻的設計,希望有生之年能見他們一次!
  • 作為一個完美主義者,我在 Part C 部分卻沒有拿到滿分,這簡直是無法忍受的。但是我著實學業繁忙,不能在這個實驗耗費太多時間,只能暫且擱置,暑假回來繼續幹它!
  • 本實驗耗時 3 天,約 17 小時

相關文章