[笔记]计算机基础 5 CSAPP Lab4-ArchitectureLab

ArchLab是CSAPP的第四个实验,主要考察对于架构的理解,根据虚拟的Y86-64指令架构,从而理解CPU与指令。
ArchLab同时涉及第4章架构和第5章优化,决定分两次完成,本博客也决定分两次完成。
从进度上来说:第4章相关的A/B部分只要一个晚上就可以完成了,没有遇到太多困难。

文章目录

  • Lab
    • A Part
      • sum.ys
      • rsum.ys
      • copy.ys
    • B Part
    • C Part
      • CPE 15.18
      • CPE=11.70 使用iaddq
      • CPE=12.33 条件传送【放弃】
      • CPE=9.93 4 x 4循环展开
      • CPE=7.82 10 x 1循环展开 + 多余指令删除 + 3 x 1剩余处理
      • CPE=7.49 分10种情况讨论剩余元素
  • 总结

Lab

A Part

A Part感觉上是主要考察对于Y86-64的“汇编”编写能力,同时还回顾了“过程”这一机制,对于递归能有更深的理解。

sum.ys

sum.ys迭代地对链表的元素求和,与下述的examples.c里的sum_list理应一致。

typedef struct ELE {
    long val;
    struct ELE *next;
} *list_ptr;

long sum_list(list_ptr ls)
{
    long val = 0;
    while (ls) {
	val += ls->val;
	ls = ls->next;
    }
    return val;
}

根据终止条件进行变成,注意CC的设置。同时还回顾了一下while的先jmp的视线,具体结果如下,也有较详细的注释:

# 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        #sum(ele1)
    ret

# long sum_list(list_ptr ls)
# ls in %rdi

sum_list:   xorq %rax,%rax  # val = 0
    andq    %rdi,%rdi       # Set CC
    jmp     test            # Goto test
loop:   mrmovq  (%rdi),%r10 # Get val
    addq    %r10,%rax       # Add to sum
    mrmovq  0x8(%rdi),%rdi  # ptr = ptr->next
    andq    %rdi,%rdi       # Set CC
test:       jne loop        # Stop when 0
    ret


# Stacks
    .pos 0x200
stack:

运行:

usr@ub2004:~/csapplab/archlab/archlab-handout/sim/misc$ ./yis sum.yo

结果:

Stopped in 26 steps at PC = 0x13.  Status 'HLT', CC Z=1 S=0 O=0
Changes to registers:
%rax:   0x0000000000000000      0x0000000000000cba
%rsp:   0x0000000000000000      0x0000000000000200
%r10:   0x0000000000000000      0x0000000000000c00

Changes to memory:
0x01f0: 0x0000000000000000      0x000000000000005b
0x01f8: 0x0000000000000000      0x0000000000000013

其中%rax结果为cba,满足条件。

rsum.ys

rsum.ys递归地对链表的元素求和,与下述的examples.c里的rsum_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;
    }
}

这里考察了“过程”这一机制,利用栈保存原有的结果,并在返回时利用:

# 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        #sum(ele1)
    ret

# long rsum_list(list_ptr ls)
# ls in %rdi

rsum_list:  pushq %rbx  # save ls->val
    mrmovq  (%rdi),%rbx # val = ls->val
    irmovq  0,%rax      # sum = 0
    andq    %rdi,%rdi   # Set CC
    je      result
    mrmovq  0x8(%rdi),%rdi  # ls = ls->next
    call    rsum_list
    addq    %rbx,%rax   # %rax = val + rest
result:
    popq    %rbx
    ret
           
# Stacks
    .pos 0x200
stack:

运行:

usr@ub2004:~/csapplab/archlab/archlab-handout/sim/misc$ ./yis rsum.yo

结果:

Stopped in 43 steps at PC = 0x13.  Status 'HLT', CC Z=0 S=0 O=0
Changes to registers:
%rax:   0x0000000000000000      0x0000000000000cba
%rsp:   0x0000000000000000      0x0000000000000200

Changes to memory:
0x01b8: 0x0000000000000000      0x0000000000000c00
0x01c0: 0x0000000000000000      0x0000000000000090
0x01c8: 0x0000000000000000      0x00000000000000b0
0x01d0: 0x0000000000000000      0x0000000000000090
0x01d8: 0x0000000000000000      0x000000000000000a
0x01e0: 0x0000000000000000      0x0000000000000090
0x01f0: 0x0000000000000000      0x000000000000005b
0x01f8: 0x0000000000000000      0x0000000000000013

其中%rax结果为cba,满足条件。

copy.ys

copy.ys对数组进行复制,与下述的examples.c里的copy_block理应一致。

/* copy_block - 将src拷贝到dest并返回xor校验和 */
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;
}

这里发现没有IADDQ,却是用起来有点麻烦,只能用寄存器保存const + OPQ来进行:

# Execution begins at address 0 
	.pos 0
	irmovq stack, %rsp  	# Set up stack pointer
	call main		# Execute main program
	halt			# Terminate program 

# Source block
.align 8
src:
        .quad   0x00a
        .quad   0x0b0
        .quad   0xc00
# Destination block
dest:
        .quad   0x111
        .quad   0x222
        .quad   0x333

main:   irmovq  src,%rdi
        irmovq  dest,%rsi
        irmovq  $3,%rdx
        call copy_block
        ret

# long copy_block(long *src, long *dest, long len)
# src in %rdi, dest in %rsi, len in %rdx

copy_block: xorq %rax,%rax      # result=0
            irmovq $8,%r8       # const 8
            irmovq $1,%r9        # const 1
            andq %rdx,%rdx      # Set CC
            jmp  test
loop:       mrmovq (%rdi),%r10  # val=*src
            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

# Stacks
    .pos 0x200
stack:

运行:

usr@ub2004:~/csapplab/archlab/archlab-handout/sim/misc$ ./yis copy.yo

结果:

Stopped in 34 steps at PC = 0xb6.  Status 'HLT', CC Z=1 S=0 O=0
Changes to registers:
%rax:   0x0000000000000000      0x0000000000000cba
%rsp:   0x0000000000000000      0x00000000000001f0
%rsi:   0x0000000000000000      0x0000000000000048
%rdi:   0x0000000000000000      0x0000000000000030
%r8:    0x0000000000000000      0x0000000000000008
%r9:    0x0000000000000000      0x0000000000000001
%r10:   0x0000000000000000      0x0000000000000c00

Changes to memory:
0x0030: 0x0000000000000111      0x000000000000000a
0x0038: 0x0000000000000222      0x00000000000000b0
0x0040: 0x0000000000000333      0x0000000000000c00
0x01f0: 0x0000000000000000      0x000000000000006f
0x01f8: 0x0000000000000000      0x0000000000000013

其中%rax结果为cba,且内存0x0030-0x0040都成功复制为对应的元素。

B Part

CSAPP的IADDQ对应格式如下:

            0    1     2                     10
iaddq V,rB  [C 0][F rB][          V          ]
阶段 OPq rA,rB irmovq V,rB iaddq V,rB
取指 icode:ifun ← M1[PC]
rA:rB ← M1[PC+1]

valP ← PC+2
icode:ifun ← M1[PC]
rA:rB ← M1[PC+1]
valC ← M8[PC+2]
valP ← PC+10
icode:ifun ← M1[PC]
rA:rB ← M1[PC+1]
valC ← M8[PC+2]
valP ← PC+10
译码 valA ← R[rA]
valB ← R[rB]

valB ← R[rB]
执行 valE ← valB OP valA
Set CC
valE ← 0 + valC valE ← valB + valC
Set CC
访存
写回 R[rB] ← valE R[rB] ← valE R[rB] ← valE
更新PC PC ← valP PC ← valP PC ← valP

这样我们就很清楚,我们需要在哪些阶段上添加HCL代码。最终所需修改的地方共有取指、译码、执行三个阶段,具体如下:

  • 取指阶段
bool instr_valid = icode in 
	{ INOP, IHALT, IRRMOVQ, IIRMOVQ, IRMMOVQ, IMRMOVQ,
	       IOPQ, IJXX, ICALL, IRET, IPUSHQ, IPOPQ, IIADDQ };		# note: add IIADDQ

# Does fetched instruction require a regid byte?
bool need_regids =
	icode in { IRRMOVQ, IOPQ, IPUSHQ, IPOPQ, 
		     IIRMOVQ, IRMMOVQ, IMRMOVQ, IIADDQ };					# note: add IIADDQ

# Does fetched instruction require a constant word?
bool need_valC =
	icode in { IIRMOVQ, IRMMOVQ, IMRMOVQ, IJXX, ICALL, IIADDQ };	# note: add IIADDQ
  • 译码阶段
## What register should be used as the B source?
word srcB = [
	icode in { IOPQ, IRMMOVQ, IMRMOVQ, IIADDQ  } : rB;		# note: add IIADDQ
	icode in { IPUSHQ, IPOPQ, ICALL, IRET } : RRSP;
	1 : RNONE;  # Don't need register
];

## What register should be used as the E destination?
word dstE = [
	icode in { IRRMOVQ } && Cnd : rB;
	icode in { IIRMOVQ, IOPQ, IIADDQ} : rB;					# note: add IIADDQ
	icode in { IPUSHQ, IPOPQ, ICALL, IRET } : RRSP;
	1 : RNONE;  # Don't write any register
];
  • 执行阶段
## Select input A to ALU
word aluA = [
	icode in { IRRMOVQ, IOPQ } : valA;
	icode in { IIRMOVQ, IRMMOVQ, IMRMOVQ, IIADDQ } : valC;			# note: add IIADDQ
	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;					# note: add IIADDQ
	icode in { IRRMOVQ, IIRMOVQ } : 0;
	# Other instructions don't need ALU
];

## Should the condition codes be updated?
bool set_cc = icode in { IOPQ, IIADDQ };							# note: add IIADDQ

回归测试运行:

1、测试除了iaddq和leave之外所有内容:

usr@ub2004:~/csapplab/archlab/archlab-handout/sim/seq$ (cd ../ptest; make SIM=../seq/ssim)
./optest.pl -s ../seq/ssim 
Simulating with ../seq/ssim
  All 49 ISA Checks Succeed
./jtest.pl -s ../seq/ssim 
Simulating with ../seq/ssim
  All 64 ISA Checks Succeed
./ctest.pl -s ../seq/ssim 
Simulating with ../seq/ssim
  All 22 ISA Checks Succeed
./htest.pl -s ../seq/ssim 
Simulating with ../seq/ssim
  All 600 ISA Checks Succeed

2、测试iaddq

usr@ub2004:~/csapplab/archlab/archlab-handout/sim/seq$ (cd ../ptest; make SIM=../seq/ssim)
./optest.pl -s ../seq/ssim -i
Simulating with ../seq/ssim
  All 58 ISA Checks Succeed
./jtest.pl -s ../seq/ssim -i
Simulating with ../seq/ssim
  All 96 ISA Checks Succeed
./ctest.pl -s ../seq/ssim -i
Simulating with ../seq/ssim
  All 22 ISA Checks Succeed
./htest.pl -s ../seq/ssim -i
Simulating with ../seq/ssim
  All 756 ISA Checks Succeed

C Part

在这一部分中,将在目录sim/pipe中工作。我们需要修改ncopy函数,使其并行度增加,并且提高运行的效率。ncopy函数将len元素整数数组src复制到一个不重叠的dst,重新计算src中包含的正整数数。

CPE 15.18

下列分别是ncopy的c代码和主要部分原始ys代码(CPE=15.18)。

int ncopy(int *src, int *dst, int len)
{
    int count = 0;
    int val;

    while (len > 0) {
	val = *src++;
	*dst++ = val;
	if (val > 0)
	    count++;
	len--;
    }
    return count;
}
# % rdi = src, %rsi =dst, %rdx = len
ncopy:
	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, %rdo		# src++
	addq %r10, %rsi		# dst++
	andq %rdx,%rdx		# len > 0?
	jg Loop			# if so, goto Loop:

CPE=11.70 使用iaddq

显然记数+1与迭代地址变化,没有iaddq就必须使用irmovq+opq共2个指令+额外1个寄存器实现,显然实现iaddq有助于我们提高运算效率。
仿照B部分,对pipe_full.hcl进行修改,修改部分如下:

bool instr_valid = f_icode in 
	{ INOP, IHALT, IRRMOVQ, IIRMOVQ, IRMMOVQ, IMRMOVQ,
	  IOPQ, IJXX, ICALL, IRET, IPUSHQ, IPOPQ, IIADDQ };			#note

bool need_regids =
	f_icode in { IRRMOVQ, IOPQ, IPUSHQ, IPOPQ, 
		     IIRMOVQ, IRMMOVQ, IMRMOVQ, IIADDQ };				#note

bool need_valC =
	f_icode in { IIRMOVQ, IRMMOVQ, IMRMOVQ, IJXX, ICALL, IIADDQ };	#note

word d_srcB = [
	D_icode in { IOPQ, IRMMOVQ, IMRMOVQ, IIADDQ  } : D_rB;		#note
	D_icode in { IPUSHQ, IPOPQ, ICALL, IRET } : RRSP;
	1 : RNONE;  # Don't need register
];

word d_dstE = [
	D_icode in { IRRMOVQ, IIRMOVQ, IOPQ, IIADDQ} : D_rB;		#note
	D_icode in { IPUSHQ, IPOPQ, ICALL, IRET } : RRSP;
	1 : RNONE;  # Don't write any register
];

word aluA = [
	E_icode in { IRRMOVQ, IOPQ } : E_valA;
	E_icode in { IIRMOVQ, IRMMOVQ, IMRMOVQ, IIADDQ } : E_valC;		#note
	E_icode in { ICALL, IPUSHQ } : -8;
	E_icode in { IRET, IPOPQ } : 8;
	# Other instructions don't need ALU
];

word aluB = [
	E_icode in { IRMMOVQ, IMRMOVQ, IOPQ, ICALL, 
		     IPUSHQ, IRET, IPOPQ, IIADDQ } : E_valB;				#note
	E_icode in { IRRMOVQ, IIRMOVQ } : 0;
	# Other instructions don't need ALU
];

bool set_cc = E_icode in {IOPQ, IIADDQ} &&						#note: E_icode == IOPQ -> E_icode in {IOPQ, IIADDQ}
	# State changes only during normal operation
	!m_stat in { SADR, SINS, SHLT } && !W_stat in { SADR, SINS, SHLT };

将所有irmovq C,%r10 + opq %r10,%rB全部更替为iaddq C,%rA,修改后得到的CPE=11.70,结果如下:

	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 $8, %rdi		# src++
	iaddq $8, %rsi		# dst++
	iaddq $-1,%rdx		# len--
	jg Loop				# if so, goto Loop:

CPE=12.33 条件传送【放弃】

可以看到修改后的代码,使用jle和jg这种控制转移,一旦预测失败就会面临分支惩罚,所以很自然地一个想法就是用条件传送来优化,代码如下:

	xorq %rax,%rax		# count = 0;
	andq %rdx,%rdx		# len <= 0?
	irmovq $1,%r11		# %r11 = const 1
	jle Done		    # if so, goto Done:

Loop:	mrmovq (%rdi), %r10	# read val from src...
	rmmovq %r10, (%rsi)	# ...and store it to dst
	xorq %r12,%r12		# set take=0
	andq %r10, %r10		# val <= 0?
	cmovg %r11,%r12		# if val>0,take=1; else take=0
	addq %r12,%rax	    # count++
	iaddq $8, %rdi		# src++
	iaddq $8, %rsi		# dst++
	iaddq $-1,%rdx		# len--
	jg Loop			# if so, goto Loop:

出乎意料地,最终结果CPE=12.33,效果反而变差了,这说明由于在Y86-64下使用条件传送所需的指令更多,这部分的开销超过了预测惩罚,所以我们确定了一个基调:不使用条件传送。

CPE=9.93 4 x 4循环展开

修改了一些细枝末节后,我们直接进入重头戏:对于循环内指令并行度的提升,直接使用循环展开,这里采用4 x 4的循环展开。

4. 4x4循环展开 CPE=9.93
	xorq %rax,%rax		# count = 0;
	xorq %rcx,%rcx		# count2 = 0;
	xorq %rbx,%rbx		# count3 = 0;
	xorq %r8,%r8		# count4 = 0;
	rrmovq %rdx,%r9		# limit=len;
	iaddq $-3,%r9			# limit=len-3;
	jle After		# if so, goto After:

Loop:	mrmovq (%rdi), %r10	
	rmmovq %r10, (%rsi)	
	mrmovq $8(%rdi),%r11
	rmmovq %r11,$8(%rsi)
	mrmovq $16(%rdi),%r12
	rmmovq %r12,$16(%rsi)
	mrmovq $24(%rdi),%r13
	rmmovq %r13,$24(%rsi)  # 4 x 4 loop expand
	andq %r10, %r10		# val <= 0?
	jle b1		# if so, goto Npos:
	iaddq $1,%rax	# count++
b1: andq %r11,%r11
	jle b2
	iaddq $1,%rcx
b2: andq %r12,%r12
	jle b3
	iaddq $1,%rbx
b3: andq %r13,%r13
	jle Npos:
	iaddq $1,%r8
Npos:   iaddq $32, %rdi		
	iaddq $32, %rsi		# i=i+4
	iaddq $-4,%rdx		# len-=4
	iaddq $-4,%r9		# limit-=4
	jg Loop				# if so, goto Loop:

	andq %rdx,%rdx		# len = 0
	je a1
After:
	mrmovq (%rdi), %r10	
	rmmovq %r10, (%rsi)	
	andq %r10, %r10		# val <= 0?
	jle Npos2		# if so, goto Npos:
	iaddq $1,%rax	# count++
Npos2:   iaddq $8, %rdi		# src++
	iaddq $8, %rsi		# dst++
	iaddq $-1,%rdx		# len--
	jg After				# if so, goto After:
a1:
	addq %rcx,%rax
	addq %rbx,%r8
	addq %r8,%rax

最终测得CPE=9.93,这里我犯了两个错误:

  • mrmovq和rmmovq连续使用,这触发了数据读写相关的加载使用冒险,使得两条指令之间插入了气泡bubble,大大拖累了效率。
  • 没有意识到这里由于分支语句,%rax的增加并不是关键路径,完全不必使用n x n的循环展开,使用n x 1的循环展开,并且使用了n x n的循环展开后,在最后答案的合并上也耗费了更多的指令。

CPE=7.82 10 x 1循环展开 + 多余指令删除 + 3 x 1剩余处理

	iaddq $-10,%rdx		# limit=len-K;
	jl loop9			

loop10:
	mrmovq (%rdi),%rcx
	mrmovq $8(%rdi),%rbx
	mrmovq $16(%rdi),%rbp
	mrmovq $24(%rdi),%r8
	mrmovq $32(%rdi),%r9
	mrmovq $40(%rdi),%r10
	mrmovq $48(%rdi),%r11
	mrmovq $56(%rdi),%r12
	mrmovq $64(%rdi),%r13
	mrmovq $72(%rdi),%r14	# 10 x 1 loop expand

	iaddq $80,%rdi

	rmmovq %rcx,(%rsi)
	rmmovq %rbx,$8(%rsi)
	rmmovq %rbp,$16(%rsi)
	rmmovq %r8,$24(%rsi)
	rmmovq %r9,$32(%rsi)
	rmmovq %r10,$40(%rsi)
	rmmovq %r11,$48(%rsi)
	rmmovq %r12,$56(%rsi)
	rmmovq %r13,$64(%rsi)
	rmmovq %r14,$72(%rsi)	# 10 x 1 loop expand

	iaddq $80,%rsi

one:
	andq %rcx,%rcx
	jle two
	iaddq $1,%rax
two:
	andq %rbx,%rbx
	jle three
	iaddq $1,%rax
three:
	andq %rbp,%rbp
	jle four
	iaddq $1,%rax
four:
	andq %r8,%r8
	jle five
	iaddq $1,%rax
five:
	andq %r9,%r9
	jle six
	iaddq $1,%rax
six:
	andq %r10,%r10
	jle seven
	iaddq $1,%rax
seven:
	andq %r11,%r11
	jle eight
	iaddq $1,%rax
eight:
	andq %r12,%r12
	jle nine
	iaddq $1,%rax
nine:
	andq %r13,%r13
	jle ten
	iaddq $1,%rax
ten:
	andq %r14,%r14
	jle iter10
	iaddq $1,%rax

iter10:
	iaddq $-10,%rdx		# len-=10
	jg loop10			# if so, goto Loop:

loop9:
	iaddq $8,%rdx		# len=len+10-2		
	jle Nloop2			# if len <= 2 ,len = 0 or len = 1 or len = 2 =》 len = -2 or len =-1 or len=0
						# remain >=3 means using 3 x 1 loop expand
loop3:
	mrmovq (%rdi),%rcx
	mrmovq $8(%rdi),%rbx
	mrmovq $16(%rdi),%rbp 	# 3 x 1 loop expand

	iaddq $24,%rdi

	rmmovq %rcx,(%rsi)
	rmmovq %rbx,$8(%rsi)
	rmmovq %rbp,$16(%rsi) 	# 3 x 1 loop expand

	iaddq $24,%rsi

loop3one:
	andq %rcx,%rcx
	jle loop3two
	iaddq $1,%rax
loop3two:
	andq %rbx,%rbx
	jle loop3three
	iaddq $1,%rax
loop3three:
	andq %rbp,%rbp
	jle loop3iter
	iaddq $1,%rax

loop3iter:
	iaddq $-3,%rdx		# len-=10
	jg loop3			# if so, goto Loop:

Nloop2:
	iaddq $1,%rdx      # if len == 2 not jmp
	jle Nloop1		   # remain = 0 or remain = 1

	mrmovq (%rdi),%rcx		# remain = 2
	mrmovq 8(%rdi),%rbx
	rmmovq %rcx,(%rsi)
	rmmovq %rbx,(%rsi)

Nloop2one:
	andq %rcx,%rcx
	jle Nloop2two
	iaddq $1,%rax
Nloop2two:
	andq %rbx,%rbx
	jle Done
	iaddq $1,%rax

Nloop1:
	iaddq $1,%rdx
	jle Done

	mrmovq (%rdi),%rcx
	iaddq $8,%rdi
	rmmovq %rcx,(%rsi)
	iaddq $8,%rsi

Nloop1one:
	andq %rcx,%rcx
	jle Done
	iaddq $1,%rax

最终这一版的CPE=7.82,已经很接近满分7.5了。
这里主要的细节如下:

  • 相同寄存器的mrmovq和rmmovq进行隔开,避免加载使用冒险,从而流水线上的执行、译码同时进行,从而塞满流水线。
  • %rax的默认值为0,可以省略开头xorq指令
  • 采用10 x 1循环展开的原因在于,除了%rax、%rdi、%rbx、%rsi以及不宜使用的%rsp(若要使用必须采用push+pop),Y86-64只剩下10个寄存器。虽然可以采用指令隔开的方法,从而进行更多的循环展开,但对于程序的性能已经没有提升了,在benchmark.pl的结果中可以看到主要的CPE集中于剩余部分
  • 由于目前程序受限于剩余部分,即在循环之外的部分,我们需要思考对于个数不超过9个剩余元素的处理,这里采用了3x1的循环展开,采用新的预测分支,一支使用3x1循环展开处理大于3个的情况,另一支采用了分类讨论的方法对0、1、2三种情况的处理。由于第一支经过3x1循环展开后,剩余部分必然<3,自然那地向下走入另一支分支,从而处理了所有的剩余元素情况。

CPE=7.49 分10种情况讨论剩余元素

很自然地,延续上一条的处理流程,在benchmark.pl的测试结果,大头CPE集中于<10的情况,这意味着循环已经优化足够了,必须更加有效地处理剩余元素部分

	iaddq $-10,%rdx		# limit=len-K;
	jl remain			

loop10:
	mrmovq (%rdi),%rcx
	mrmovq $8(%rdi),%rbx
	mrmovq $16(%rdi),%rbp
	mrmovq $24(%rdi),%r8
	mrmovq $32(%rdi),%r9
	mrmovq $40(%rdi),%r10
	mrmovq $48(%rdi),%r11
	mrmovq $56(%rdi),%r12
	mrmovq $64(%rdi),%r13
	mrmovq $72(%rdi),%r14	# 10 x 1 loop expand

	rmmovq %rcx,(%rsi)
	rmmovq %rbx,$8(%rsi)
	rmmovq %rbp,$16(%rsi)
	rmmovq %r8,$24(%rsi)
	rmmovq %r9,$32(%rsi)
	rmmovq %r10,$40(%rsi)
	rmmovq %r11,$48(%rsi)
	rmmovq %r12,$56(%rsi)
	rmmovq %r13,$64(%rsi)
	rmmovq %r14,$72(%rsi)	# 10 x 1 loop expand

one:
	andq %rcx,%rcx
	jle two
	iaddq $1,%rax
two:
	andq %rbx,%rbx
	jle three
	iaddq $1,%rax
three:
	andq %rbp,%rbp
	jle four
	iaddq $1,%rax
four:
	andq %r8,%r8
	jle five
	iaddq $1,%rax
five:
	andq %r9,%r9
	jle six
	iaddq $1,%rax
six:
	andq %r10,%r10
	jle seven
	iaddq $1,%rax
seven:
	andq %r11,%r11
	jle eight
	iaddq $1,%rax
eight:
	andq %r12,%r12
	jle nine
	iaddq $1,%rax
nine:
	andq %r13,%r13
	jle ten
	iaddq $1,%rax
ten:
	andq %r14,%r14
	jle iter10
	iaddq $1,%rax

iter10:
	iaddq $80,%rdi
	iaddq $80,%rsi
	iaddq $-10,%rdx		# len-=10
	jg loop10			# if so, goto Loop:

remain:
	iaddq $7,%rdx		# recover len <- len+10 and compare len with 3
	jl left
	jg right
	jmp remain3

left:
	iaddq $2,%rdx
	je remain1
	iaddq $-1,%rdx	
	je remain2
	ret

right:
	iaddq $-3,%rdx		# compare with 6
	jg remain789		# remain >=7
	je remain6			# remain ==6

	iaddq $1,%rdx		# remain == 4 / 5
	je remain5
	jmp remain4

remain789:
	iaddq $-2,%rdx
	jl remain7
	je remain8      	# jg remain9 is default



remain9:
	mrmovq 64(%rdi),%r8
	andq %r8,%r8
	rmmovq %r8,64(%rsi)	

remain8:
	mrmovq 56(%rdi),%r8
	jle remain82
	iaddq $1,%rax

remain82:
	rmmovq %r8,56(%rsi)
	andq %r8,%r8
remain7:
	mrmovq 48(%rdi),%r8
	jle remain72
	iaddq $1,%rax
remain72:
	rmmovq %r8,48(%rsi)
	andq %r8,%r8
remain6:
	mrmovq 40(%rdi),%r8
	jle remain62
	iaddq $1,%rax
remain62:
	rmmovq %r8,40(%rsi)
	andq %r8,%r8
remain5:
	mrmovq 32(%rdi),%r8
	jle remain52
	iaddq $1,%rax
remain52:
	rmmovq %r8,32(%rsi)
	andq %r8,%r8
remain4:
	mrmovq 24(%rdi),%r8
	jle remain42
	iaddq $1,%rax
remain42:
	rmmovq %r8,24(%rsi)
	andq %r8,%r8
remain3:
	mrmovq 16(%rdi),%r8
	jle remain32
	iaddq $1,%rax
remain32:
	rmmovq %r8,16(%rsi)
	andq %r8,%r8
remain2:
	mrmovq 8(%rdi),%r8
	jle remain22
	iaddq $1,%rax
remain22:
	rmmovq %r8,8(%rsi)
	andq %r8,%r8
remain1:
	mrmovq (%rdi),%r8
	jle remain12
	iaddq $1,%rax
remain12:
	rmmovq %r8,(%rsi)
	andq %r8,%r8
	jle Done
	iaddq $1,%rax

最终测得CPE=7.49,这里有意思的地方如下:

  • 首先直接采用树状结构的分支跳转,会增加指令数,应该尽可能地利用顺序执行这一特性,上述代码中就将remain 4和5的情况自然地放在6和7的处理之后,从而省略一个jmp指令。
  • 因为示例代码对于1/2/3/4的测试往往较高,最终发现以3为底最终的效果要优于以4/5的传统二分为底

总结

到Lab4的C Part,终于遇到了无论如何都无法进一步优化的情况,不得不参考了他人的博客。

  • 一方面在于,第5章我看的比较快,虽然在终于熬过了痛苦的前4章后,第5章的内容又流畅又愉快,所以不到3天就看完了,但确实基础可能还没那么牢固。
  • 另一方面,尽信书则不如无书,CSAPP中n x 1展开不改变关键路径,效率提升不如n x n循环展开让我绕进了死胡同,中途揪着寄存器与内存IO、条件传送、局部变量死凹,无论如何都无法将CPE降到9以下,却完全忽略了nx1循环展开的可能性。

但还是很愉快地看到在痛苦的2/3/4章后,终于有一章让我有如鱼得水的快乐了,接下来的第6章Cache我早有耳闻,准备在考试之后,认认真真地慢慢精读,以结束第一部分!
——2023.4.17

你可能感兴趣的:(算法,计算机,cpu,csapp,archlab)