Skip to main content

CSAPP Chapter 4

챕터
#

2026

CSAPP Architecture Lab

·2974 words·14 mins
📝 상세 정리 # Part A # sum.ys # Y86_64 문법으로 연결리스트의 원소들을 더하는 C 코드를 바꾸자. .pos 0 irmovq stack, %rsp call main halt .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 sum_list: irmovq $0, %rax sum_list_while: andq %rdi, %rdi je sum_list_while_end mrmovq (%rdi), %rbx addq %rbx, %rax mrmovq 8(%rdi), %rdi jmp sum_list_while sum_list_while_end: ret .pos 0x200 stack: rsum.ys # sum.ys와 같지만, 재귀함수로 구성하자. .pos 0 irmovq stack, %rsp call main halt .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 rsum_list: andq %rdi, %rdi jne rsum_list_func irmovq $0, %rax ret rsum_list_func: pushq %rbx mrmovq (%rdi), %rbx mrmovq 8(%rdi), %rdi call rsum_list addq %rbx, %rax popq %rbx ret .pos 0x200 stack: copy.ys # 이번엔 source 블럭에서 destination 블럭으로 값을 복사하는걸 수행하자 .pos 0 irmovq stack, %rsp call main halt .align 8 src: .quad 0x00a .quad 0x0b0 .quad 0xc00 dest: .quad 0x111 .quad 0x222 .quad 0x333 main: irmovq src, %rdi irmovq dest, %rsi irmovq $3, %rdx call copy_block ret copy_block: xorq %rax, %rax // result = 0 copy_block_while: andq %rdx, %rdx je copy_block_end irmovq $8, %rcx mrmovq (%rdi), %rbx // val = *src addq %rcx, %rdi // src++ rmmovq %rbx, (%rsi) // *dest = val addq %rcx, %rsi // dest++ xorq %rbx, %rax // result ^= val irmovq $1, %rbx subq %rbx, %rdx jmp copy_block_while copy_block_end: ret .pos 0x200 stack: Part B # SEQ 프로세서 (seq-full.hcl)에 iaddq 명령어를 추가하자. Fetch / Decode / Execute / Mmory / Write-back를 잘 처리해야겠네… 천천히 해보자 Fetch # ################ Fetch Stage ################################### # Determine instruction code word icode = [ imem_error: INOP; 1: imem_icode; # Default: get from instruction memory ]; # Determine instruction function word ifun = [ imem_error: FNONE; 1: imem_ifun; # Default: get from instruction memory ]; bool instr_valid = icode in { INOP, IHALT, IRRMOVQ, IIRMOVQ, IRMMOVQ, IMRMOVQ, IOPQ, IJXX, ICALL, IRET, IPUSHQ, IPOPQ }; # Does fetched instruction require a regid byte? bool need_regids = icode in { IRRMOVQ, IOPQ, IPUSHQ, IPOPQ, IIRMOVQ, IRMMOVQ, IMRMOVQ }; # Does fetched instruction require a constant word? bool need_valC = icode in { IIRMOVQ, IRMMOVQ, IMRMOVQ, IJXX, ICALL }; iaddq를 뭐가 필요할까? 일단 icode, ifun 은 물론 잘 있다고 생각하고, 아래를 생각해보자. instr_valid를 true로 만들기 위해 저기에다가 IIADDQ를 추가하고, 레지스터가 필요한가? 아무래도 레지스터에 더해야한다. val_C도 필요한가? 아무래도 상수값을 더하려면 필요하다. 다 추가해주자. Decode # ################ Decode Stage ################################### ## What register should be used as the A source? word srcA = [ icode in { IRRMOVQ, IRMMOVQ, IOPQ, IPUSHQ } : rA; icode in { IPOPQ, IRET } : RRSP; 1 : RNONE; # Don't need register ]; ## What register should be used as the B source? word srcB = [ icode in { IOPQ, IRMMOVQ, IMRMOVQ } : rB; 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} : rB; icode in { IPUSHQ, IPOPQ, ICALL, IRET } : RRSP; 1 : RNONE; # Don't write any register ]; ## What register should be used as the M destination? word dstM = [ icode in { IMRMOVQ, IPOPQ } : rA; 1 : RNONE; # Don't write any register ]; srcA, srcB, dstE, dstM중에선 뭐가 필요할까? 아무래도 가져올필요 없으니 srcA, srcB는 필요 없는거같고, dst만 있으면 될것같다. 아니지, srcB에다가 더해야하는거니까 srcB도 가져와야한다. 메모리에서 긁어온게 아니라 Execute에서 얻을 수 있는 값이니, dstE에 추가하면 되겠다. 근데 Cnd는 뭐지? condition중에 하나인거같은데, 일단 무조건 rB는 되어야하니 두번째줄에 넣자. Execute # ################ Execute Stage ################################### ## Select input A to ALU word aluA = [ icode in { IRRMOVQ, IOPQ } : valA; icode in { IIRMOVQ, IRMMOVQ, IMRMOVQ } : 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 } : valB; icode in { IRRMOVQ, IIRMOVQ } : 0; # Other instructions don't need ALU ]; ## Set the ALU function word alufun = [ icode == IOPQ : ifun; 1 : ALUADD; ]; ## Should the condition codes be updated? bool set_cc = icode in { IOPQ }; 계산이랑 조건비트 설정까지 다 해야하는 Execute 단계이다. aluA에 valC를, aluB에 valB를 넣어야겠지? alufun은 아니고, setcc는 켜야겠다. Memory # 여기선 뭐 할게 없죠 메모리에 접근을 안하는디 Write back # 에? 원본 코드에는 여기가 없다. 알아서 되는 영역인듯? Program Counter Update # 이것도 자연스럽게 처리되어있다. Test # 위의것들을 추가하면 간단하게 완료된다! 생각보다 쉽네 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 ./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 Part C # ncopy.ys가 최대한 빠르게 실행대도록 최적화하는 단계이다. CPE가 7.5 이하로 가도록! pipe-full.hcl를 수정하고, ncopy.ys를 수정하자. make VERSION=full GUIMODE="" # pipe-full.hcl 수정 후 make drivers # ncopy.ys 수정 후 ./correctness.pl # 정확성 확인 ./benchmark.pl # CPE 확인 처음 CPE는 15.18이다. 힘내보자고 pipe-full.hcl # 일단 CPU부터 손보자. 여기서도 IIADDQ를 추가해야하는것 같다. Fetch # 일단 f_PC는 상관 없을 것 같다. f_icode, f_ifun도 문제 없고.. instr_valid는 추가해야겠지. 위와 같이 need_regids, need_valC도 다 추가해야겠다. f_predPC는 점프할일 없으니까 f_valP 그대로! Decode # 똑같이 d_srcB, d_dstE 두개를 남겨야할 것 같다. valA를 가져올때 이슈인데… 포워딩을 해야되는데. valA에는 Execute 단계에서 valC를 가져오면 되고, valB는 땡겨올텐데… 아 이미 코딩이 되어있다. e_dstE, M_dstM, M_dstE, W_dstM, W_dstE 순서에서 땡겨온다. 안건드려도 될듯 Execute # aluA는 E_valC에서 데려오는거니, 거기다가 IIADDQ를 넣어주자. aluB에는 E_valB에서 가져오게. alufun은 상관없고, setcc가 좀 이슈인데… ## Should the condition codes be updated? bool set_cc = E_icode == IOPQ && # State changes only during normal operation !m_stat in { SADR, SINS, SHLT } && !W_stat in { SADR, SINS, SHLT }; 원래 코드가 이렇게 되어있는데, E_icode == IOPQ 부분을 in 문법으로 바꾸면 될까? bool set_cc = E_icode in { IOPQ, IIADDQ} && 이렇게 한번 바꿔보자. Memory # 할일 없지 않을까? Pipeline Register conrtrol # 버블을 넣을까 말까인데, IADDQ는 뭐 상관 없었지. stall도 할 필요 없고. test # ❯ cd ../y86-code; make testpsim Makefile:42: warning: ignoring prerequisites on suffix rule definition Makefile:45: warning: ignoring prerequisites on suffix rule definition Makefile:48: warning: ignoring prerequisites on suffix rule definition Makefile:51: warning: ignoring prerequisites on suffix rule definition ../pipe/psim -t asum.yo > asum.pipe ../pipe/psim -t asumr.yo > asumr.pipe ../pipe/psim -t cjr.yo > cjr.pipe ../pipe/psim -t j-cc.yo > j-cc.pipe ../pipe/psim -t poptest.yo > poptest.pipe ../pipe/psim -t pushquestion.yo > pushquestion.pipe ../pipe/psim -t pushtest.yo > pushtest.pipe ../pipe/psim -t prog1.yo > prog1.pipe ../pipe/psim -t prog2.yo > prog2.pipe ../pipe/psim -t prog3.yo > prog3.pipe ../pipe/psim -t prog4.yo > prog4.pipe ../pipe/psim -t prog5.yo > prog5.pipe ../pipe/psim -t prog6.yo > prog6.pipe ../pipe/psim -t prog7.yo > prog7.pipe ../pipe/psim -t prog8.yo > prog8.pipe ../pipe/psim -t ret-hazard.yo > ret-hazard.pipe grep "ISA Check" *.pipe asum.pipe:ISA Check Succeeds asumr.pipe:ISA Check Succeeds cjr.pipe:ISA Check Succeeds j-cc.pipe:ISA Check Succeeds poptest.pipe:ISA Check Succeeds prog1.pipe:ISA Check Succeeds prog2.pipe:ISA Check Succeeds prog3.pipe:ISA Check Succeeds prog4.pipe:ISA Check Succeeds prog5.pipe:ISA Check Succeeds prog6.pipe:ISA Check Succeeds prog7.pipe:ISA Check Succeeds prog8.pipe:ISA Check Succeeds pushquestion.pipe:ISA Check Succeeds pushtest.pipe:ISA Check Succeeds ret-hazard.pipe:ISA Check Succeeds rm asum.pipe asumr.pipe cjr.pipe j-cc.pipe poptest.pipe pushquestion.pipe pushtest.pipe prog1.pipe prog2.pipe prog3.pipe prog4.pipe prog5.pipe prog6.pipe prog7.pipe prog8.pipe ret-hazard.pipe ❯ cd ../ptest; make SIM=../pipe/psim TFLAGS=-i ./optest.pl -s ../pipe/psim -i Simulating with ../pipe/psim All 58 ISA Checks Succeed ./jtest.pl -s ../pipe/psim -i Simulating with ../pipe/psim All 96 ISA Checks Succeed ./ctest.pl -s ../pipe/psim -i Simulating with ../pipe/psim All 22 ISA Checks Succeed ./htest.pl -s ../pipe/psim -i Simulating with ../pipe/psim All 756 ISA Checks Succeed 구웃 잘 돌아간다 ncopy.ys # 일단 아무래도 CPE는 그대로 15.18이다. 이걸 잘 바꿔보자. # 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: 우리가 바꿀 수 있는건 이부분인데.. IIADDQ # irmovq 를 이용해서 레지스트리에 값을 넣고, 그걸 addq로 연산하는 부분이 두군데 보인다. 이걸 iaddq로 줄일 수 있겠다. 이걸 수행했더니 CPE가 15.18 -> 13.70으로 줄었다. 갈길이 멀어보인다.. 어라, Npos에도 $-1로 len–를 구현할 수 있었다. 딱 하나 줄어서 12.70이 됐다. 순서 바꾸기? # mmmovq가 없으니, mrmovq, rmmovq로 데이터를 복사하고 있다. 그런데 이때 메모리 참조에서 하자드가 일어나서, bubble / stall이 들어가지 않았던가? 이 사이에 andq를 넣어볼까? 아 이것만으로는 같다… 안되넹 Loop: mrmovq (%rdi), %r10 # read val from src... andq %r10, %r10 # val <= 0? rmmovq %r10, (%rsi) # ...and store it to dst jle Npos # if so, goto Npos: iaddq $1, %rax # count++ 아하, 안되는게 아니라 이렇게 수정한거였는데, andq에서도 결국 %r10이 필요해서 이슈가 생긴다. 그런데 사이에 더 끼울수있는게 없는데.. 어셈블리 최적화 # 코드 자체를 최적화해볼까? 점프같은걸 잘 없앨 수 있을거같은데. 점프 적중률을 올려보자. Y86-64는 언제나 점프를 안한다고 생각하고 움직인다. 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... andq %r10, %r10 # val <= 0? rmmovq %r10, (%rsi) # ...and store it to dst # jle Npos # if so, goto Npos: # iaddq $1, %rax # count++ jg plus1 Npos: iaddq $-1, %rdx # len-- iaddq $8, %rdi # src++ iaddq $8, %rsi # dst++ andq %rdx,%rdx # len > 0? jle Done jmp Loop # if so, goto Loop: plus1: iaddq $1, %rax jmp Npos 쩝 이런느낌으로 해볼까 했는데, 오히려 명령어가 늘어서 15를 넘겨버렸다. 어떻게 하면 좋지? 점프를 덜타야할거같은데 버킷질 # 제곱근분할법에서 하는것처럼, 버킷질로 묶어서 처리할 수 있지 않을까? 그렇게하면 mrmovq 해저드도 없앨 수 있지 않을까? Loop: mrmovq (%rdi), %r10 # read val from src... mrmovq 8(%rdi), %r11 # src+1 rmmovq %r10, (%rsi) # ...and store it to dst rmmovq %r11, 8(%rsi) # src+1 to dst+1 andq %r10, %r10 # val <= 0? jle Npos # if so, goto Npos: iaddq $1, %rax # count++ Npos: andq %r11, %r11 jle Npos2 iaddq $1, %rax Npos2: iaddq $-2, %rdx # len-- iaddq $16, %rdi # src++ iaddq $16, %rsi # dst++ andq %rdx,%rdx # len > 0? jg Loop # if so, goto Loop: 아 다 좋은데.. 이게 짝수개일때만 동작한다. 홀수개이면 어카지? Loop2를 만들까? Loop로는 len > 1일때만 보내자. ncopy: xorq %rax,%rax # count = 0; iaddq $-1, %rdx jl Done Loop: je move1 mrmovq (%rdi), %r10 # read val from src... mrmovq 8(%rdi), %r11 # src+1 rmmovq %r10, (%rsi) # ...and store it to dst rmmovq %r11, 8(%rsi) # src+1 to dst+1 andq %r10, %r10 # val <= 0? jle Npos # if so, goto Npos: iaddq $1, %rax # count++ Npos: andq %r11, %r11 jle Npos2 iaddq $1, %rax Npos2: iaddq $16, %rdi # src += 2 iaddq $16, %rsi # dst += 2 iaddq $-2, %rdx # len -= 2 jge Loop jmp Done move1: mrmovq (%rdi), %r10 # read val from src... rmmovq %r10, (%rsi) # ...and store it to dst andq %r10, %r10 # val <= 0? jle Done # if so, goto Npos: iaddq $1, %rax # count++ 오!! 이렇게하니까 된다!! CPE 10.08까지 줄였다. 배치가 0~64개까지 있으니까, 대충 8개정도가 제곱근 분할법상 맞는거같지만.. %r10부터 시작하니, %r10~r15까지 6개를 써서 해볼까? 하아 코드가 개길어진다 ncopy: xorq %rax,%rax # count = 0; isbig: iaddq $-6, %rdx jge Loop iaddq $6, %rdx router: # check remainder 0 to 5 iaddq $-1, %rdx jl Done je move1 iaddq $-2, %rdx jl move2 je move3 iaddq $-2, %rdx jl move4 je move5 Loop: mrmovq (%rdi), %r10 mrmovq 8(%rdi), %r11 mrmovq 16(%rdi), %r12 mrmovq 24(%rdi), %r13 mrmovq 32(%rdi), %r14 mrmovq 40(%rdi), %r9 rmmovq %r10, (%rsi) rmmovq %r11, 8(%rsi) rmmovq %r12, 16(%rsi) rmmovq %r13, 24(%rsi) rmmovq %r14, 32(%rsi) rmmovq %r9, 40(%rsi) Npos_loop1: andq %r10, %r10 jle Npos_loop2 iaddq $1, %rax Npos_loop2: andq %r11, %r11 jle Npos_loop3 iaddq $1, %rax Npos_loop3: andq %r12, %r12 jle Npos_loop4 iaddq $1, %rax Npos_loop4: andq %r13, %r13 jle Npos_loop5 iaddq $1, %rax Npos_loop5: andq %r14, %r14 jle Npos_loop6 iaddq $1, %rax Npos_loop6: andq %r9, %r9 jle Loop_end iaddq $1, %rax Loop_end: iaddq $48, %rdi iaddq $48, %rsi iaddq $-6, %rdx jge Loop iaddq $6, %rdx jmp router move1: mrmovq (%rdi), %r10 rmmovq %r10, (%rsi) jmp move_check10 move2: mrmovq (%rdi), %r10 mrmovq 8(%rdi), %r11 rmmovq %r10, (%rsi) rmmovq %r11, 8(%rsi) jmp move_check11 move3: mrmovq (%rdi), %r10 mrmovq 8(%rdi), %r11 mrmovq 16(%rdi), %r12 rmmovq %r10, (%rsi) rmmovq %r11, 8(%rsi) rmmovq %r12, 16(%rsi) jmp move_check12 move4: mrmovq (%rdi), %r10 mrmovq 8(%rdi), %r11 mrmovq 16(%rdi), %r12 mrmovq 24(%rdi), %r13 rmmovq %r10, (%rsi) rmmovq %r11, 8(%rsi) rmmovq %r12, 16(%rsi) rmmovq %r13, 24(%rsi) jmp move_check13 move5: mrmovq (%rdi), %r10 mrmovq 8(%rdi), %r11 mrmovq 16(%rdi), %r12 mrmovq 24(%rdi), %r13 mrmovq 32(%rdi), %r14 rmmovq %r10, (%rsi) rmmovq %r11, 8(%rsi) rmmovq %r12, 16(%rsi) rmmovq %r13, 24(%rsi) rmmovq %r14, 32(%rsi) move_check14: andq %r14, %r14 jle move_check13 iaddq $1, %rax move_check13: andq %r13, %r13 jle move_check12 iaddq $1, %rax move_check12: andq %r12, %r12 jle move_check11 iaddq $1, %rax move_check11: andq %r11, %r11 jle move_check10 iaddq $1, %rax move_check10: andq %r10, %r10 jle Done iaddq $1, %rax 열심히 했더니 8.01까지 줄었다!! 이분 탐색 # router_tree: # now 0 <= rdx <= 5 iaddq $-3, %rdx je move3 jg router_tree_R router_tree_L: # rdx < 3 iaddq $2, %rdx jl Done je move1 jg move2 router_tree_R: # rdx > 3 iaddq $-1, %rdx je move4 jg move5 라우터를 이분탐색으로 해봤고, 7.80까지 줄일 수 있었다. move 로직도 좀 수정했다. ################################################################## # You can modify this portion # Loop header xorq %rax,%rax # count = 0; isbig: iaddq $-6, %rdx jge Loop router: iaddq $3, %rdx jl router_L je move3 jg router_R router_L: iaddq $2, %rdx jl Done je move1 jmp move2 router_R: iaddq $-1, %rdx je move4 jmp move5 Loop: mrmovq (%rdi), %r10 mrmovq 8(%rdi), %r11 mrmovq 16(%rdi), %r12 mrmovq 24(%rdi), %r13 mrmovq 32(%rdi), %r14 mrmovq 40(%rdi), %r9 Npos_loop1: andq %r10, %r10 rmmovq %r10, (%rsi) jle Npos_loop2 iaddq $1, %rax Npos_loop2: andq %r11, %r11 rmmovq %r11, 8(%rsi) jle Npos_loop3 iaddq $1, %rax Npos_loop3: andq %r12, %r12 rmmovq %r12, 16(%rsi) jle Npos_loop4 iaddq $1, %rax Npos_loop4: andq %r13, %r13 rmmovq %r13, 24(%rsi) jle Npos_loop5 iaddq $1, %rax Npos_loop5: andq %r14, %r14 rmmovq %r14, 32(%rsi) jle Npos_loop6 iaddq $1, %rax Npos_loop6: andq %r9, %r9 rmmovq %r9, 40(%rsi) jle Loop_end iaddq $1, %rax Loop_end: iaddq $48, %rdi iaddq $48, %rsi iaddq $-6, %rdx jge Loop jmp router move1: mrmovq (%rdi), %r10 jmp move_check10 move2: mrmovq (%rdi), %r10 mrmovq 8(%rdi), %r11 jmp move_check11 move3: mrmovq (%rdi), %r10 mrmovq 8(%rdi), %r11 mrmovq 16(%rdi), %r12 jmp move_check12 move4: mrmovq (%rdi), %r10 mrmovq 8(%rdi), %r11 mrmovq 16(%rdi), %r12 mrmovq 24(%rdi), %r13 jmp move_check13 move5: mrmovq (%rdi), %r10 mrmovq 8(%rdi), %r11 mrmovq 16(%rdi), %r12 mrmovq 24(%rdi), %r13 mrmovq 32(%rdi), %r14 move_check14: andq %r14, %r14 rmmovq %r14, 32(%rsi) jle move_check13 iaddq $1, %rax move_check13: andq %r13, %r13 rmmovq %r13, 24(%rsi) jle move_check12 iaddq $1, %rax move_check12: andq %r12, %r12 rmmovq %r12, 16(%rsi) jle move_check11 iaddq $1, %rax move_check11: andq %r11, %r11 rmmovq %r11, 8(%rsi) jle move_check10 iaddq $1, %rax move_check10: andq %r10, %r10 rmmovq %r10, (%rsi) jle Done iaddq $1, %rax 7.73정도까지 줄였는데.. 진짜 더는 못하겠다. 멀 해도 안줄어든다 ㅠ.ㅠ 여기서 포기 ❔질문 사항 # 🔗 참고 자료 #

CSAPP 4.6 Summary

·156 words·1 min
📝 상세 정리 # 4.6.0 Intro # 우리는 ISA가 프로세서의 동작과 프로세서의 구현 방식 사이의 추상화를 제공함을 보았다. Y86-64 ISA는 x86-64 명령어를 기반으로, 대폭 단순화해서 정의했다. RISC, CISC 명령어 집합의 특성을 모두 지니고 있다. 파이프라이닝을 통해 시스템 처리량의 성능을 향상시켰다. SEQ를 재배열하여 SEQ+를, 파이프라인 레지스터를 추가하여 PIPE를 만들었다. 결과 전송 속도를 높이기 위해 포워딩 로직을 추가했다. 여러 틁수한 경우에 파이프라인 단계를 정지 / 취소하는 제어 로직도 만들었다 우리가 배운 프로세서 설계에 관한 몇가지 교훈은 다음과 같다. 복잡성 관리는 최우선 과제이다. 최소비용으로 최대성능을 얻기 위해 ISA를 직접 구현할 필요가 없다. 같은 ISA를 구현하는 방식에 대한 제약을 두지 어ㅏㄴㅎ는다는 의미이다. 세심해야한다. 칩이 제조되면 수정이 불가능하므로, 체계적인 시뮬레이션 테스트가 필요하다. 4.6.1 Y86-64 Simulators # 시뮬레이터의 제어 로직은 로직 블럭에 대한 HCL 선언을 C러ㅗ 변환하여 생성된다. 테스트 스크립트도 있을 것이다. ❔질문 사항 # RISC, CISC? CISC(Complex Instruction Set Computer) -> 명령어 하나에 많은 일을 담자 RICS(Riduces Instruction Set Computer) -> 명령어를 단순하고 균일하게. 컴파일러가 조합하게 하자.

CSAPP 4.5 Pipelnied Y86-64 Implementations

·2147 words·11 mins
📝 상세 정리 # 4.5.0 Intro # 이제 설계를 시작할 준비가 되었다. SEQ를 소폭 수정하여 PC계산을 fetch 단계로 옮기자. 파이프라인 레지스터를 추가하자. 아직 종속성을 제대로 처리하지 못하지만, 일단 효율적인 파이프라인을 만들어보자. 4.5.1 SEQ+: Rearranging the computation Stages # SEQ의 5단계를 약간 재배열해서 PC업데이트 단계가 클록 주기의 끝이 아닌 시작에 오도록 하자. 이 설계를 SEQ+라고 한다. PC를 클록 주기의 시작에 오게 하기 위해선, 이전의 icode, Cnd, valC, valM, valP등이 필요하다. 이젠 이 다섯개를 하드웨어 레지스터를 따로 둬서 저장하면 되겠다. 이걸 회로 리타이밍이라고 한다. 4.5.2 Inserting Pipeline Registers # 중간에 stat, icode, ifun, rA, rB, valC, valA, valP, dstE… 등 여러가지를 저장하는 층을 만들자. SEQ의 단계와 같지만, 단계 사이를 파이프라인 레지스터가 분리하고 있는것이다. 그 층의 이름은 다음과 같다. F: PC의 예측값 보유 D: Fetch와 Decode 사이에서, 최근에 인출된 명령어에 대한 정보 보유 E: Decode와 Execute 사이에서, 가장 최근에 디코드된 명령어에 대한 정보와 레지스터파일에서 읽은 값 보유 M: Execute와 Memory 사이에서, 메모리단계에서 처리할 가장 최근에 실행된 명령어의 결과 보유 조건부 점프 처리를 위한 분기조건 / 대상에 대한 정보도 보유 W: Memory와 Write back 단계 사이에서, 계산된 결과를 레지스터 파일에 쓰기 위한 정보 보유 ret 명령어 완료시 PC 선택 로직에 반환 주소 공급 4.5.3 Rearranging ans Relabeling Signals # SEQ와 SEQ+는 한번에 하나의 명령어만 처리하므로 valC, rscA, valE와 같은 신호에 고유한 값 존재 하지만 파이프라인 설계에서는 서로 다른 명령어와 연관된 여러 버전 존재 올바른 버전의 신호를 사용하도록 주의하지 않으면, 오류가 발생할 수 있다. 따라서 D_stat, E_stat.. 처럼 명명하자. 하드웨어 레지스터가 아닌 그 안에서 계산되는 stat과 같은 부분도 있다! 이는 소문자로 f_stat, m_stat과 같이 쓰자. 전체 프로세서의 실제 상태는 W_stat을 기반으로 Write Back 단계에서 계산된다. SEQ+, PIPE- 의 디코드단계는 모두 valE, valM용 dstE, dstM 신호를 생성한다. SEQ+에서는 이 신호를 레지스터 파일 쓰기 포트에 직접 꽂았지만 PIPE-에서는 이 신호가 파이프라인을 통해 전달되어, Write Back 단계에서 신호가 전달되면 그때 수행한다. PIPE에서 SEQ+에 없는게 하나 있다. Select A 블럭 이 블럭은 파이프라인 레지스터 D의 valP 혹은 레지스터파일의 A포트에서 값을 선택하여 valA를 결정한다. 이는 파이프라인 레지스터 E와 M으로 전달되어야 하는 상태 양을 줄이기 위해서이다. call 명령어만이 메모리단계에서 valP를 필요로 한다. 조건부 점프만이 Execute 단계에서 valP를 필요로 한다. (안뛸떄) 두 경우 모두 레지스터파일을 읽은 값을 쓰지 않는다. 따라서 이 신호를 병합하여 valA를 통해 전달하면서, 파이프라인 레지스터의 상태 양을 줄일 수 있다. 4.5.4 Next PC Prediction # 파이프라인 설계의 목표는 매 클럭 사이클마다 새로운 명령어를 발행하는 것 이를 달성하기 위해선, 현재 명령을 가져온 직후 다음 명령어의 위치를 결정해야함 하지만 가져온 명령어가 조건부 분기라면, 명령어가 실행 단계를 통과한 후에나 알 수 있다. 가져온 명령어가 ret라면 메모리 단계를 통과한 후에나 알 수 있따. 나머지는 모두 Fetch 단계에서 계산된 정보를 기반으로 다음 명령어의 주소를 결정할 수 있다. call과 jmp의 경우에는 valC, 그 외의 경우에는 valP가 된다. 따라서 PC의 다음 값을 예측하면서, 클럭마다 새로운 명령어를 발행시키자. 조건부는 무조건 뛴다 or 무조건 안뛴다 등으로 결정해버리면 된다. 예측이 틀렸을때 어떻게 하는지는 4.5.8장에서 다루겠다. 이 방법을 분기 예측 (branch prediction) 이라고 한다. 우리는 뭄조건 뛴다고 예측하고 해보자. 따라서 PC를 valC로 예측하자. ret는.. 이슈가 깊다. 여기에서는 거의 모든 값이 올 수 있으니까. 따라서, 우리 설계에서는 ret에 대해서는 예측하지 않고, Write back이 끝날때까지 명령어 처리를 보류하겠다. 이것도 4.5.8에서 다루자. Fetch 단계를 보면 Predict PC에 valP, valC가 꽂혀있는걸 볼 수 있다. 이걸 F_predPC에 저장한다. F_predPC와 M_valA, W_valM, M_cnd를 이용해서 Select PC를 진행한다. ret일때는 W_valM, jump할때는 M_valA로 가던가 하겠다. 4.5.5 Pipeline Hazards # 아직 우리는 종속성 문제를 해결하지 못했다. 종속성은 두가지 형태로 나타난다. 데이터 종속성 한 명령어에 의해 계산된 결과가 후속 명령어의 데이터로 사용되는 경우 제어 종속성 한 명령어가 후속 명령어의 위치를 결정하는 경우 jump, call, ret일때 이 위험성들을 우리는 hazard라고 한다. 이도 똑같이 데이터 hazard, 제어 hazard라고 한다. Data hazard irmovq $10, %rdx irmovq $3, %rax (여러개의 nop) addq %rdx, %rax halt 와 같은 코드가 있었다고 할 때, nop의 개수에 따라 오류가 발생할 수도 안발생할 수도 있다. 예를들어 nop가 세개 이상이라면 문제가 없지만, 없다면? addq가 rdx, rax를 읽을때 위의 명령어들의 Write back 단계에 가지 못했기 때문 Avoid Data Hazards by Stalling 위험조건이 더이상 성립하지 않을 때까지 파이프라인 내 하나 이상의 명령어를 대기시키는 것 소스 연산자를 생성하는 명령어가 쓰기 Write back 단계를 통과할 때 까지 Decode 단계의 명령어를 대기시키면 된다. addq 명령어가 Decode 단계에 있을 때, Execute, Memory, Write back 단계에 있는 명령어중 하나가 %rdx나 %rax를 업데이트 할 것을 감지하면, 명령어를 스테일링해서 Decode 단계에서 대기시킨다. 물론 이때 뒤의 Fetch단계에 있는 명령어도 보류된다. 이는 실행단계에 버블을 주입해서 처리한다. 이는 동적으로 생긴 nop와 같다. 상세 메커니즘은 4.5.8에서 살펴보자. 그러나 이러한 방법은 구현은 쉽지만 아무래도 결과적인 성능이 좋진 않다. 3사이클이나 쉬게 되니까 Avoiding Data Hazards by Forwarding PIPE는 기본적으로 소스 연산자를 Decode 단계에서 레지스터 파일에서 읽지만, 해당 소스 레지스터중 하나에 대한 쓰기 작업이 Wrtie back 단계에서 대기중일 수도 있다! 써질때까지 기다리는 대신, 곧 쓰일 값을 거기서 Decode 단계에 훔쳐와서 써버리자. 이런 방법을 Data Forwarding / Forwarding / Bypassing 이라고 한다. 데이터 포워딩을 구현하기 위해선 기본적인 하드웨어 구조에 추가적인 데이터 연결 및 제어 로직이 있어야 한다. 이렇게 데이터 해저드를 포워딩으로 처리할 수 있는 PIPE를 PIPE+라고 한다. Load/Use Data Hazards Memory 작업이 파이프라인 후반부에 있기에, 포워딩만으로 처리할 수 없는 한 해저드 클래스가 존재한다. mrmovq 0(%rdx), %rax addq %ebx, %eax 위와 같이 메모리에서 읽어온 값을 그대로 써야한다고 해보자. 이건 데이터 포워딩으로 불가능할 것 같다. 어떻게 해야하지? 이건 어쩔수없이 Stalling 해야한다. Stall 한번 후 Memory 단계에서 Forward로 데려오면 되겠다! 이런 load/use Hazard를 피하기 위한 stall + Forward를 load interlock이라고 한다. 이것만이 파이프라인 처리량을감소시키므로, 매 클럭 사이클마다 새로운 명령어를 발행한다는 목표는 거의 도달했다고 볼 수 있다. Control Hazards 이는 위에서 말한것과 같이 Fetch단계의 현재 명령어를 기반으로 다음 명령어의 주소를 신뢰할 수 없을때 ret / jump 에 대해서만 이슈 특히 jump는 예측을 실패했을때만 이슈 ret는 fetch 후 write back 단계에 돌입할때까지 (Memory가 끝날때까지) bubble을 주입해서 다음 PC를 계산할 수 있도록 기다린다. bubble 3개! jump가 이슈인데, 이는 일단 분기 예측을 했다고 가정하자. 분기 예측을 실패하면, 몇개의 잘못된 명령어들이 Fetch, Decode에 들어가있는 상태이다. 그러면 이 잘못된 명령어 / 계산값들을 버려야한다. 따라서 디코드 및 실행 단계에 버블을 주입해서, 이 명령어들을 취소 (instruction squashing) 할 수 있다. 우리가 관찰 가능한 상태에는 문제가 안생기고, 두 클럭이 낭비된다는 단점만 있다. 구체적인 방법은 4.5.8장에서 보자. (왤케 미루냐) 4.5.6 Exception Handling # 프로세서 내의 다양한 활동이 예외 상황으로 갈 수도 있다. 이건 프로그램에 의해 내부적으로 / 혹은 외부 신호에 의해 외부적으로 생성될 수 있다. 우리 ISA에는 다음과 같은 내부 생성 예외가 있다. 정지 명령어 (halt) 명령어와 함수 코드의 유효하지 않은 조합 명령어 페치 또는 데이터 읽기/쓰기에서 유효하지 않은 주소에 대한접근 보다 완전한 프로세서라면, 네트워크 인터페이스가 새 패킷을 수신하거나 / 사용자가 클릭을 했다는 외부 예외도 처리해야 한다. 예외를 발생시킨 명령어를 예외 발생 명령어 (excepting instruction) 이라고 부르자. 실제 이런 명령어는 존재하지 않지만, 유효하지 않은 주소에 가상 명령어가 존재한다고 생각하면 쉽다. 예외에 도달하면 정지하고, 적절한 상태 코드를 설정해야한다. 파이프라인 시스템에서 예외처리는 몇가지 미묘한 세부 사항을 수반한다 여러 명령어에 의해 동시에 예외가 발생할 수 있다. Fetch / Memory 단계에서 동시에 발생할 수 있지 그러면 어떤걸 먼저 보고할지 결정해야 한다. 기본 규칙은 가장 먼저 진행된 명령어에 의해 트리거된 예외에 우선 순위를부여하는 것 그러면 Memory겠네 명령어가 처음 Fetch되어 시작되고 예외를 발생시킨 후, 잘못된 분기예측으로 인해 취소된 경우 이 명령어를 취소하지만, 예외도 발생시키지 않도록 해야한다. fetch조차 되면 안되는 명령어였기 때문 예외를 발생시키는 명령어 다음에 오는 명령어가 해당 예외 명령어가 완료되기 전에 시스템 상태의 일부에 영향을 줄 수 있다. Memory 단계에서 예외를 발견했지만, 이때 Execute에서 조건코드같은걸 수정한 경우 예외 발생 이후에는 어떤 명령어도 시스템 상태에 영향을 미치면 안되니까 4.5.7 PIPE Stage Implementations # 이제 포워딩을 갖춘 Y86-64 PIPE의 전체 구조를 완성했다. PC_Relection and fetch Stage PC에 대한 현재 값을 선택하고 다음 PC값을 예측 Select PC에서 될 수 있는 값은 F_predPC (웬만한 경우) M_valA (예측 실패한 분기가 메모리단계에 진입할떄 / ret가 아닌 valP) W_valM (ret 명령어가 워크백 단계에 진입할 때) Decode and Write-Back Stages dstE, dstM, srcA, srcB 를 잘 관리하기 포워딩을 신경써야한다. 포워딩 소스는 총 5개가 존재한다. e_valE -> e_dstE m_valM -> M_dstM M_valE -> M_dstE W_valM -> W_dstM W_valE -> W_dstE 이 5개중에 해당하지 않으면 블럭은 레지스터 포트 A에서 읽은 d_rvalA를 출력하면 된다. 위의 순서대로 우선순위가 잡혀있는데, 이는 상당히 중요하다. Execute Stage SEQ때랑 비슷하다. 조건 코드를 업데이트하는 set CC가 m_stat과 W_stat 신호를 입력으로 받는것만 차이 파이프라인 앞단계에 예외 상태인 명령어가 있으면 조건코드 업데이트를 막기 위해! Memory Stage SEQ때랑 거의 동일하다. 4.5.8 Pipeline Control logic # 이제 데이터 포워딩 / 분기예측과 같은 메커니즘으로 처리할 수 없는 네가지 제어 사례를 처리하고, PIPE의 설계를 완료하자. Load/use Hazard 한사이클 정지해야함 ret ret이 write-back에 도달할때까지 파이프라인이 정지해야함 잘못된 예측 분기 명령어들을 취소하고, 점프명령어 다음 명령어에서 명령어 추출이 시작되어야함 Exception 예외가 발생해씅랟, 관찰 가능한 상태의 업데이트를 비활성화해야함 Desired Handling of Special Control Cases load/use 4.5.5에서 배운것과 같이, mrmovq, popq만이 데이터를 읽는데 이들 중 하나가 execute 스테이지에 있고 목적지 레지스터를 필요로하는 명령어가 decode에 있을때 대기시키고 다음 사이클의 실행단계에 bubble을 주입해야 한다. 파이프라인 레지스터 D, F를 고정해야 한다. ret 3사이클 대기시키면 된다. 잘못된 예측 분기 D, E단계에 버블을 주입한다. 올바른 명령어를 fetch한다. 예외 발생 파이프라인 구현이 원하는 ISA동작과 일치하도록 한다. 이게 좀 어렵다. 예외가 프로그램 실행중 Fetch / Memory 두단계에서 발생할 수 있고 프로그램 상태가 Execute / Memory / write-back 세곳에서 업데이트되기 때문 각 단계에는 stat이 포함되어있어서, 명령어마다 그 상태를 추적한다. 예외가 발생하면 해당 정보를 상태 일부로 기록하고, 메모리 단계에 도달하면, 후속 명령어가 관찰 가능한상태를 수정하지 못하도록 Execute 단계의 명령어가 조건 코드를 설정하지 못하도록 비활성화 Memory 단계에 버블을 주입하여 데이터 메모리로의 쓰기 비활성화 Write back 단계에 예외를 유발하는 명령어가 있을 경우 정지 write-back이 레지스터 파일을 갱신하는 유일한 단계이기 때문! Detecting Special Control Conditions 이런 특수 상황이 발생하는 조건 load/use E_icode ∊ {IMRMOVQ, IPOPQ} && E_dstM ∊ {d_srcA, d_srcB} ret IRET ∊ {D_icode, E_icode, M_icode} 잘못된 예측 분기 E_icode = IJXX&& !e_Cnd 예외 m_stat ∊ {SADR, SINS, SHLT} || W_stat ∊ {SADR, SINS, SHLT} Pipeline Control Mechanisms 클럭이 튈때, stall과 bubble에 따라 갱신 / 고정 / nop화를 할 수 있겠다 stall과 bubble을 동시에 1로 설정하는것은 오류로 간주한다. Combinations of Control Conditions 현실에서는 여러 특수조건이 동시에발생할 수도 있다. 하지만 우리가 상호배타적으로 세팅을 잘 해놔서, 뜰만한 중첩이 많이 없다. 해봤자 예측실패와 ret1, load/use와 ret1정도. 앞을 조합 A, 뒤를 조합 B라고 하자. 조합 A에서는 분기가 잘못 예측되었음을 감지하고 ret를 취소한다. 조합 B에서, ret때문에 버블을 넣으면 좀 곤란해진다. load/use에서는 Decode를 stall하려고하고 ret때문에는 Decode 단계에 버블이 들어가니까 ret를 위해 한 사이클 지연되어야한다. Control logic implementation 이를 모두 관리하는 Pipeline control Logic이라는게 있어야한다. 여기서 F_stall, D_bubble, D_stall, E_bubble…등을 관리하고 D_icode, d_srcA, d_srcB, e_Cnt..등을 받아서 처리해야한다. 4.5.9 Performance Analysis # 파이프라인 제어 로직때문에 우리 목적이었던 매 클럭마다 명령어 하나 처리하기를 못하게 되긴한다. 이건 버블의 주입 빈도를 측정함으로써 정량화할 수 있다. 이 측정을 CPI (Cycle Per Instruction)이라고 한다. 이 측정은 파이프라인의 평균 처리 속도의 역수이지만, 시간은 피코초가 아닌 클럭 사이클 단위로 측정된다. $C_i$를 명령어 개수, $C_b$를 버블 개수라고 하면 $CPI = \frac{C_i + C_b}{c_i} = 1.0 + \frac{C_b}{C_i}$ 따라서 명령어당 버블 비율로 나타낼 수 있는데, 버블이 들어가는 예이ㅗ는 세가지가 있으므로 $CPI = 1.0 + lp + mp + rp$ load penelty / mispredicted branch penalty / return penalty 이때 비율들로 계산할 수 있겠다. 4.5.10 Unfinished Business # 아직도 누락된 사항들이 존재한다. Multicycle Instructions 정수 곱셈 / 나눗셈이나 부동소수점 연산을 수행하는 명령어도 구현해야 한다. ALU를 사용해서 실행단계 로직의 기능을 확장하면 되겠지만, 성능이 너무 낮아진다. 나눗셈의 경우 64사이클이나 걸릴수도 있다.. 그래서 독립적으로 작동하는 특수 하드웨어 기능 유닛을 사용한다. 물론 파이프라이닝도 되어있다 Interfacing with the Memory System 명령어 인출과 데이터메모리 모두 한 메모리 위에 있고 가상주소를 참조하기전에 실제주소를 가야하고… 메모리값이 디스크에 있어서 수백만 클럭 사이클이 필요할수도 이건 6, 9장에서 나중에 더 자세히 배울 것이다. 운영체제, 가상메모리, 캐시 등등등 ❔질문 사항 # 하드웨어 레지스터가 수율이 낮거나 만들기 비싼가? 파이프라인에서도 최대한 조금만 쓰려고 노력하는 걸로 보인다. 돈보다는 면적과 전력 문제인데, 뭐 대충 맞다. 꽤나 중요한 최적화다.

CSAPP 4.4 General Principles of Pipelining

·316 words·2 mins
📝 상세 정리 # 4.4.0 Intro # 파이프라인은 웬만한 사람에게 익숙할 것이다. 급식소는 샐러드 / 요리 / 디저트 / 음료등을 나눠서 제공하고 세차장도 물과 비누를 분사하고 / 닦고 / 왁스를 바르고 / 건조하고.. 여러 고객이 동시에 시스템을 통과하도록 허용하는 것. 파이프라이닝의 핵심 특징은 시스템의 처리량을 증가시킨다는 것 하지만 개별 고객에 대한 서비스시간 (대기시간)은 약간 증가될 수도 있다. 4.4.1 Computational Pipelines # 컴퓨터로 오면, 고객은 명령어이고, 각 단계는 명령어 실행의 일부 부분이다. 현대 논리 설계에서는 회로 지연을 피코초 ($ps, 10^{-12}$초) 단위로 측정한다. 처리량은 초당 기가 명령어 단위로 표현된다. 명령어를 끝까지 수행하는데 필요한 총 시간을 지연(latency)라고 하고, 이는 처리량의 역수이다. 4.4.2 A Detailed Look at Pipleline Operation # 중간에 레지스터층을 둬서, 입력과 출력을 적당히 저장해서 파이프라이닝을 할 수 있겠다. 클럭이 느려지는건 문제가 없겠지만,너무 빨라지면 레지스터 입력값들이 유효하지 않아질 수 있다. 4.4.3 Limitations of Pipelining # 하지만 3단계로 쪼갠다고 3배 빨라지지는 않는다, 불행히도. Nonuniform Partitioning 아무래도, 세 단계로 쪼갰을때 각 단계를 1/3씩은 못맞추고, 300이었던게 50, 150정도로 쪼개지겠지. 근데 그러면 클럭을 작동할 수 있는 속도는 가장 느린 단계에 맞춰진다. 300이 50 / 100 / 150으로 쪼개진다면 150에 맞춰지고, 중간 레지스터 저장까지 생각하면 170ps정도밖에 성능 개선이 안일어난다. Diminishing Returns of Deep Pipelining 그러면 많이 쪼개면 좋은걸까? 그러면, 레지스터 저장 오버헤드가 커진다. 잘 쪼개서 300ps를 50ps 6단계로 쪼갰다고 생각해보자. 이때 레지스터에 의해 50 + 20 = 70ps정도로 된다. 전체 지연에서 28%나 레지스터 지연이 차지한다. 현대 프로세서에서는 그래도 클럭 속도를 극대화하기 위해 15단계 이상의 깊은 파이프라인을 사용하긴 한다. 4.4.4 Pipelining a System with Feedback # 우리는 지금까지 모든 명령어가 독립적이라고 가정했다. 하지만 연속된 명령어 간에 종속성이 존재할 수 있다. 하지만 파이프라이닝을 진행하면, 앞의 결과가 업데이트 되지 않았는데 뒤에서 쓰려고 하는 상황이 발생할 수 있다. 이런 피드백 효과를 적절히 처리해서, 시스템 동작이 변경되지 않도록 조심해야할 것이다. ❔질문 사항 # 그러면 램이나 CPU 오버클럭이 잘된다 / 안된다라는 말은, 오버클럭을 했을때, 얼마나 뻑이 안나냐 라는건가? 이론상 클럭을 걍 두배로 늘려버리면, 연산이 덜끝났는데 클럭이 튀어버리면 결과값이 이상해질테니까. 대충 안전한선까지 클럭을 올리는건가? 대충 맞다.

CSAPP 4.3 Sequential Y86-64 Implementations

·983 words·5 mins
📝 상세 정리 # 이제 순차적 프로세서 (SEQ)를 먼저 만들 것이다. 각 클록사이클에서, 완전한 명령어를 처리하는 모든 단계를 수행한다. 그래서 느릴 것이다! 4.3.1 Organizing Processing into Stages # 일반적으로, 명령어 처리에는 여러 연산이 수반된다. 우리는 그 모든 명령어가 일관된 단계 시퀀스를 따르도록 하고싶다. 과정은 다음과 같다 Fetch PC를 메모리주소로 사용하여 명령어 바이트 읽기 icode / ifun / 레지스터 지정자 / valC 등을 읽기 valP는 다음 명령어 주소 = PC의 값에 읽은 명령어의 길이를 더한 값 Decode 레지스터파일에서 최대 두개의 연산자를 읽어서 valA / valB 제공 Execute 명령어에 의해 지정된 연산을 수행하거나, 메모리 참조의 주소를 계산하거나, 스택포인터를 증가/감소하는 등. 이 값을 valE라고 한다. 조건 플래그가 설정 / 평가하는것도 여기 Memory 메모리에 데이터를 쓰거나 메모리에서 데이터를 읽어오기 그 값을 valM이라고 한다. Write back 최대 두개의 결과를 레지스터 파일에 쓰기 PC update PC를 다음 명령어 주소로 업데이트. 프로세서는 위 과정을 무한히 반복한다. 예외가 발생할때 정지한다 halt / 유효하지 않은 명령어 / 유효하지 않은 주소 등 보다 완전한 설계에서는 예외처리 모드에 진입해서 특수 코드를 실행 과정이 되게 많게 느껴지지만, 저렇게 전체 흐름을 유사하게 해야 하드웨어의 양을 최소화하고 복잡성을 줄일 수 있다. 서로 다른 명령어가 가능한 많은 하드웨어를 공유하도록 한다거나.. 하드웨어에서 논리블럭을 중복하는 비용은 소프트웨어에서 코드블럭을 중복하는거보다 훨씬 비싸니까! 4.3.2 SEQ Hardware Structure # 모든 Y86-64 명령어들은 위의 6단계의 연속으로 조직화할 것이다. 하드웨어적으로 위의 6단계는 어떻게 구성되는가? Fetch PC 레지스터를 이용해서 명령어 바이트 읽기, valP 계산 Decode 레지스터 파일의 두개의 읽기포트를 통해 valA, valB를 동시에 읽는다. Execute 명령어 유형에 따라 arithmetic / Logic ALU 유닛을 다양하게 사용한다. 그중에 입력중 하나에 0을 더하여 출력으로 전달하는 더미 adder도 있다! CC (Condition Code Register) 세개의 조건 코드 비트를 보유함 새로운 값은 ALU에 의해 계산되고, 조건부 이동 명령어를 실행할 때 목적지 레지스터를 업데이트할지를 계산함. 점프같은것도 마찬가지. Memory 메모리의 워드를 읽거나 씀. 명령어 메모리와 데이터 메모리는 그림에서는 다르게 그리지만, 결국 하나의 메모리에 엑세스하는것임 Write back 레지스터파일의 두개의 쓰기 포트로 데이터를 작성 포트 E는 ALU의 계산 결과를 쓰기 위해, 포트 M은 데이터 메모리에서 읽은 값을 쓰기 위해 사용 그림으로 나타낼때 관례 클럭 레지스터는 흰색 직사각형 (SEQ에서는 PC가 유일한 크럭 레지스터이다) 하드웨어 유닛은 연한 파란색 사각형 제어 로직 블럭은 회색 둥근 사각형 와이어 이름은 흰색 원 (라벨) 워드는 중간두께 선, 바이트는 얇은선, 단일비트는 점선 4.3.3 SEQ Timing # SEQ는 조합기와 두가지 형태의 메모리 장치(클록 레지스터, 랜덤 액세스 메모리)로 구성된다. 조합기는 시퀀싱이나 제어 없이, 입력이 변경될때마다 논리게이트 네트워크를 통해 전파된다. 랜덤 엑세스 메모리에서 데이터를 읽어오는 과정은 주소 입력에 기반하여 계산된다고 봐도 된다. 레지스터 파일에서는 합리적이고, 더 큰 회로에서도 특수한 클록 회로를 이용해서 이를 모방할 수 있다. 써있는 말이 좀 복잡한데, 그냥 회로 관점에서 어차피 쓰기도 클록 엣지에서만 일어나고, 읽기는 주소를 넣으면 데이터가 나오는 과정이니까 조합 논리 블록이랑 다를게 없다는 말인 것 같다. 명령어 메모리는 명령어 읽기 전용으로만 사용되므로, 이 유닛 자체를 조합기로 생각할 수 있다. 이렇게 생각하면, 시퀀싱에 대해 명시적으로 제어가 필요한건 PC, 조건코드 레지스터, 데이터 메모리, 레지스터 파일 4가지가 남는다. 이들은 클록신호 하나를 통해 제어되고, 레지스터에 값을 로드하고 랜덤액세스 메모리에 값을 쓰는 타이밍을 트리거한다. PC는 매 클록 사이클마다 새로운 명령어 주소를 로드하고, 조건코드 레지스터는 OPq에서만 로드되고, 데이터메모리는 rmmovq, pushq, call에서만 쓰이고… 레지스터 파일의 쓰기 포트는 매 사이클마다 두개의 프로그램 레지스터를 업데이트할 수 있지만, 특수 레지스터 ID인 0xF를 포트주소로 사용해서 쓰기가 없도록 할 수 있다. 이러한 클록ing는 시퀀싱을 제어하는데 꼭 필요한 것들이다. 원칙: No reading back 프로세스는 명령어 처리를 완료하기 위해 해당 명령어에 의해 업데이트 된 상태를 읽을 필요가 없다.

CSAPP 4.2 Logic design ant the Hardware Contrlop Language HCL

·484 words·3 mins
📝 상세 정리 # 4.2.0 하드웨어 상에서 전자 회로는 비트들 상의 함수를 계산하고 / 상이한 종류의 메모리 요소들에 비트들을 저장하기 위해 사용된다. 논리값 1은 1볼트 내외의 고전압, 0은 0볼트 내외의 저전압으로 표현한다. 디지털 시스템을 구현하기 위해서는 다음과 같은 세가지 주요 구성 요소가 필요하다. 비트에 대한 함수를 계산하기 위한 조합 로직 비트를 저장하기 위한 메모리 요소 메모리 요소의 업데이트를 위한 클록 신호 4.2.1 Logic Gates 논리 게이트는 디지털 회로의 기본 컴퓨팅 요소이다. bool값에 대한 and, or, not 연산 && & / || | 차이 && || 는 논리연산자 -> 결과는 0 or 1 & |는 비트연산자 -> 결과는 각 비트에 대해 수행한 값 논리게이트는 항상 활성화 되어있다. 입력값이 변경되면 잠시후에 그에따라 출력이 수정될 것 4.2.2 Combinational circuits and HCL Boolean Ecpressions 다수의 논리 게이트를 네트워크로 조립하면서 우리는 조합 회로로 알려진 계산 블록을 구성할 수 있다. 이때 몇가지 제한이 있는데 모든 논리 게이트의 입력은 다음 세가지중 정확히 하나에 연결되어야 한다. 시스템 입력(1차 입력) 일부 메모리요소의 출력 일부 논리게이트의 출력 둘 이상의 논리 게이트의 출력은 함께 연결될 수 없다. (?wire 를 상이한 전압을 향해 구동시켜서 회로 오동작을 야기할 수 있다.) 아하, 이게 무슨소린가 했는데 두 출력부를 연결하면 하나가 1, 하나가 0이었다면 연결될때 좀 곤란해진다! 전원과 접지가 만나는것도 이슈고. 네트워크는 비순환적이어야 한다. 루프는 네트워크에 의해 계산된 함수에 모호성을 야기할 수 있다. 4.2.3 Word-Level Combinational Circuits and HCL Integer Expressions 큰 논리 게이트 네트워크를 조립함으로써 우리는 훨씬 더 복잡한 함수를 계산할 수 있다. word 단위로 연산해야지! 정수, 주소, instuction 코드, 레지스터 식별자 등 4~64비트 범위의 수많은 word가 있을 것 앞으로 그림으로 나타낼때 점선은 비트단위, 중간크기 실선은 word단위 4.2.4 Set MemberShip HCL에서 or연산이 붙어있는 코드는 in 명령어를 사용할 수 있다. 4.2.5 Memory and Clocking 조합회로는 본질적으로 어떤 정보도 저장하지 않는다. 하지만 순차회로가 필요해지면, 우리는 비트로 표현되는 정보를 저장하는 장치를 도입해야 한다. 이는 새로운 값이 장치에 로드될 때를 결정하는 주기적인 신호인 단일 클럭에 의해 제어된다. 클록 레지스터는 개별 비트 / 워드를 저장함 랜덤 액세스 메모리는 주소를 사용해서 읽거나 쓸 단어를 선택하면서 여러 워드를 저장함 대표적으로 가상 메모리 시스템, 레지스터 파일 등 아무튼 이 두가지 레지스터를 각각 하드웨어 레지스터 / 프로그램 레지스터라고 하자. 레지스터 파일은 내부 저장장치를 갖기에 조합회로가 아니다. 아하, 이게 종합적으로 무슨말인지 천천히 읽으면서 이해해보자. 결국 값을 저장할 일은 분명히 생긴다. 전에 계산한 결과를 활용하던가 해야할 수 있으니까. 잘 알듯이 그걸 저장하는 부분은 CPU의 레지스터다. CPU에 달려있는 그 친구를 하드웨어 레지스터라고 부르자. 그러면 저장을 어떻게 구현하는가? CPU에서는 안에서 D-플립플랍이라는걸 이용해서, 안에 전류를 가두는 방식을 사용한다. 가두는 타이밍은 클록 신호 (0-1 진동신호)가 켜지는 그 타이밍이다. X86-64에는 총 16개의 레지스터가 있으니까, 하드웨어 레지스터는 총 16개가 필요하다. 그리고 이것들에 대한 묶음을 레지스터 파일이라고 한다. 왜 묶어서 보관하는가? 그건 16개에 대해 모든 ALU에 다는게 에바기 때문이다. 위에 MUX, 선택기마냥 이 레지스터에서도 비트마스킹 결과처럼 연산해서 값을 얻어내는게 회로로 구현하기 훨씬 더 쉽다. 따라서 그 보드 위의 레지스터를 하드웨어 레지스터, 그리고 우리가 쓰는 %rax같은걸 프로그램 레지스터라고 한다. ❔질문 사항 # 루프는 네트워크에 의해 계산된 함수에 모호성을 야기할 수 있다. 왜? AND / OR 같은 회로들로 루프를 만들면 전압이 진동하는게 아니라 중간에서 멈춰버린다!

CSAPP 4.1 The Y86-64 Instuction Set Architecture

·615 words·3 mins
📝 상세 정리 # ISA를 정의하는 것은 컴포넌트들, 명령어들, 인코딩, 프로그래밍 컨벤션 세트 등을 정의하는 것을 포함한다. 4.1.1 Programmer-Visible State Y86-64 프로그램의 각 명령어는 프로세서 상태의 일부를 판독하고 수정할 수 있다. 이를 프로그래머 가시화 (programmer-visible) 상태라고 한다. programmer은 어셈블리 코드에서 프로그램을 작성하는 사람 혹은 컴파일러를 의미함 Y86-64의 레지스터는 x86-64와 비슷하게 %rax %rbx… %r14까지 있다. 각 레지스터는 64비트 word를 저장하고, %rsp는 스택 포인터로 사용되고… 정보 조건 코드는 ZF, SF, OF 3가지가 존재한다. PC (Program Counter)은 현재 실행중인 명령어의 주소를 보유한다. 메모리는 프로그램과 데이터를 모두 유지한다. 가상 주소를 사용하여 참조 메모리 위치를 프로그램할 것 4.1.2 Y86-64 Instructions 우리가 만들 Y86-64 ISA는 x86-64의 서브셋이다. 8바이트 정수 연산만을 포함 더 적은 어드레싱 모드 더 작은 연산 세트 8바이트만 하니까, 그냥 word라고 불러도 된다! movq는 ir, rr, mr, rm movq 4가지가 있다. i는 immediate값, r은 레지스터, m은 메모리 정수연산은 addq, subq, andq, xorq 4가지로, 레지스터 데이터에만 동작하도록 할 것이다. 물론 ZF, SF, OF도 설정해야한다. 점프 명령어는 jmp, jle, jl, je, jne, jge, jg 7가지가 있다. 조건부 이동에는 cmovle, cmovl, cmove, cmovne, cmovge, cmovg 6가지가 있다. call은 스택에 return address를 push하고 jump한다. pushq, popq도 물론 있다. hlt이 나오면 상태코드가 HLT로 설정된 상태에서 프로세서가 정지된다. 4.1.3. Insturction Encoding 각 instuction에는 어떤 필드가 필요한지에 따라 1~10바이트가 필요하다. 모든 insturction은 유형을 식별하는 초기 바이트가 있는데, 여기서 위의 4비트는 코드부분, 아래 4비트는 함수 부분이다. 코드부분은 0 ~ 0xB 까지의 범위를 가진다. 함수부분은 관련 instuction이 같은 코드부분을 공유할 때만 유의하다. (OPq 등) jmp는 0x70이어야하는것처럼. 뒤의 1바이트는 레지스터 식별자이다. x86-64와 일치하게 하겠다. 뒤의 8바이트는 상수 word 영역이다. 일부 명령어는 길이가 1바이트뿐이 안되지만 피연산자가 필요하거나 상수파트가 필요하거나 하면 커진다. x86-64와 마찬가지로 다 Little Endian이다!! 예를 들어 rmmovq %rsp, 0x123456789abcd(%rdx)는 rmmovq -> 40, %rsp %rdx -> 42, 상수는 리틀엔디안 처리되어 40 42 cd ab 89 67 45 23 01 00 으로 인코딩될 것이다. 4.1.4 Y86-64 Exceptions 실행 프로그램의 전체 상태를 나타내는 상태 코드도 있다. 모두 정상인 AOK halt를 만난 HLT 잘못된 주소를 만난 ADR 잘못된 instruction을 만난 INS 4.1.5 Y86-64 Programs Y86-64는 x86-64와 달리 immediate 값을 이용한 연산이 안되기때문에, 레지스터에 로드하고 사용한다. “.“로 시작하는 word는 어셈블러에게 코드를 생성하는 주소를 조정하거나 데이터의 일부 단어를 삽입하도록 지시하는 어셈블러 지시이다. “.pos 0"은 어셈블러가 주소 0에서 시작하는 코드를 생성하기 시작해야 함을 나타낸다. 38 # Stack starts here and grows to lower addresses 39 .pos 0x200 40 stack: 이와같이 스택주소를 지정해서 더 낮은주소로 확장되도록 설정할 수도 있다. 7 # Array of 4 elements 8 .align 8 9 array : 10 .quad 0x000d000d000d 11 .quad 0x00c000c000c0 12 .quad 0x0b000b000b00 13 .quad 0xa000a000a000 배열의 시작을 나타내며, 8바이트 정렬이 이루어져 있다. 이와 같이 Y86-64를 생성하기 위한 우리의 도구는 어셈블러이기에 우리는 컴파일러, 링커, 런타임 시스템 위임 등의 작업을 수행해야 한다. 4.1.6 Some Y86-64 Instruction details pushq 명령어는 스택포인터를 모두 8만큼 감소시키고 레지스터 값을 메모리에 저장한다. 그렇다면 pushq %rsp는? 1 .text 2 .globl pushtest 3 pushtest: 4 movq %rsp, %rax Copy stack pointer 5 pushq %rsp Push stack pointer 6 Popd %rdx Pop it back 7 subq %rdx, %rax Return 0 or 4 8 ret x86-64에서, 위 코드는 언제나 0만을 반환한다. 이는 4만큼 땡기기 전의 기존 %rsp값을 push하는것을 의미한다. 1 .text 2 .globl poptest 3 poptest: 4 movq %rsp, %rdi Save stack pointer 5 pushq $0xabcd Push test value 6 popq %rsp Pop to stack pointer 7 movq %rsp, %rax Set popped value as return value 8 movq %rdi, %rsp Restore stack pointer 9 ret 해당 코드도 언제나 0xabcd만을 반환한다. 이는 또한 %rsp에 값을 넣고 %rsp를 움직임을 의미한다. ❔질문 # 인코딩된 바이트에 따라 해석해서 간단한 연산만으로 복잡한 프로그램이 돌아간다는건 알겠는데, 그래서 프로세서가 실제로 바이너리 코드들을 어떻게 연산하는거지? 프로세서는 바이너리 코드를 해석하는게 아니라 실제로 바이너리 코드가 흐르는 길 자체를 물리적으로 열고 닫는다. 예를 들어 addq %rax, %rdx과 같은 인코딩이 들어오면, 제어 유닛에서 더하기를 담당하는 ALU를 키고, %rax과 %rdx의 통로를 열어서 결국 결과값에 해당하는 전압 패턴을 얻는다.