동일한 ISA를 탑재하는 CPU라고 할지라도 내부 구현 방식은 다양할 수 있다. 특히 '명령어를 해석하고 실행하는 방식'의 관점에서 구현 방식을 나눠보면, Sequential Implementation(이하 SEQ)과 Pipelined Implementation(이하 파이프라인)이 대표적이다. SEQ가 가장 단순하지만 정확한 구현 방식이라면, 파이프라인은 성능의 개선을 위해 새롭게 고안된 구현 방식이라고 할 수 있다. 우리는 앞서 x86-64의 단순한 버전으로서 공부한 Y86-64를 기준으로, SEQ와 파이프라인이 구체적으로 어떻게 구현되는 것인지 알아볼 것이다. 이번 포스팅에서는 먼저 SEQ 방식에 대해 알아보도록 하자.
1. SEQ Overview
1-1. 프로세스 상태 (Process State)
① PC (Program Counter)
② CC (Condition Code)
③ 레지스터 파일 (Register File)
④ 메모리 (Memory) = 데이터 메모리 (Data Memory) + 명령어 메모리 (Instruction Memory)
1-2. 명령어 실행 단계 (SEQ Stages)
SEQ 방식에서는 모든 명령어들의 실행이 아래와 같은 6단계(Fetch, Decode, Execute, Memory, Write back, PC update)를 거치는 하나의 패턴으로 처리된다. 단지 명령어의 종류마다 각 단계에서 사용되는 값과 하드웨어가 다를 뿐이다. 이때 중요한 사실은, 각 단계들이 한 사이클 내에 동시에 처리된다는 것이다. 각 단계는 단순히 CPU 내부 회로상에서 특정 계산을 담당하는 한 부분일 뿐이다. 예를 들어, 조건 만족 여부를 판단하기 위해 Execute 단계에서 Cnd의 값을 계산한다는 것은 Execute 단계까지 가야 Cnd 값을 계산할 수 있다는 의미가 아니다. 단지 Execute 단계를 담당하는 부분의 논리 회로에서 Cnd 값이 계산된다는 의미이다.
단계 | 동작 | 관련 계산값 (→ 그림에서 흰색 타원) |
Fetch | 명령어 메모리에서 명령어를 읽어온다. | icode, ifun, rA, rB, valC |
ㅤ | 다음 명령어의 주소를 계산한다. | valP |
Decode | 피연산자로 사용되는 레지스터의 값을 읽어온다. | srcA, srcB, valA, valB |
ㅤ | 목적지 레지스터의 번호를 지정한다. | dstE, dstM |
Execute | ALU를 통해 단순 값 혹은 메모리 주소를 계산한다. | valE |
ㅤ | 조건 만족 여부를 판단한다. | Cnd |
Memory | 데이터 메모리에서 값을 읽어온다. | valM |
ㅤ | 데이터 메모리에 값을 쓴다. | X |
Write Back | 레지스터에 값을 쓴다. | dstE, dstM, valE, valM |
PC update | PC 값을 갱신한다. | newPC |
2. Y86-64 각 명령어의 단계별 처리 예시
2-1. OPq
2-2. rmmovq
2-3. popq
2-4. cmovXX
2-5. jXX
2-6. call
2-7. ret
3. SEQ 하드웨어 구조
3-1. SEQ 하드웨어 구조
맨 처음에 간단히 살펴본 SEQ 하드웨어 구조를 자세히 뜯어보면 다음과 같다. 파란색 박스는 프로세서 상태에 해당하는 장치들이다. 그리고 노란색 동그라미는 컨트롤 신호들로서, MUX 셀렉터 신호, 메모리/레지스터 활성화 신호, ALU 함수 결정 신호 등에 해당한다.
3-2. 컨트롤 신호의 발생 (Generating Control Signals)
그렇다면 컨트롤 신호는 어떠한 원리로 발생하는 것일까? 앞서 말했듯 컨트롤 신호의 종류로는 MUX 셀렉터 신호, 메모리/레지스터 활성화 신호, ALU 함수 결정 신호 등이 있다. 잘 생각해 보면, 이것들은 결국 해당 명령어의 실행을 처리할 때 각 단계에서 어떤 값과 하드웨어를 사용할지 결정하는 신호들이다. 따라서 99%의 컨트롤 신호는 명령어의 종류(icode)만 보고도 결정 가능하다. 명령어의 종류에 따라 각 단계에서 사용되는 값과 하드웨어가 달라지기 때문이다. 그렇다면 나머지 1%는 무엇일까?
먼저 명령어 특성상 조건 만족 여부를 판단해야 하는 경우이다.
cmovXX
와 jXX
명령어가 이에 해당한다. 이러한 경우, icode의 값 이외에도 ifun의 값과 CC의 값을 알아야 해당 동작을 수행할지 말지 결정할 수 있다. 다음으로, 명령어 실행 상태(에러 발생 여부)를 판단해야 하는 경우이다. 명령어 메모리에 잘못된 접근을 하거나(imem_error 신호 활성화), 데이터 메모리에 잘못된 접근을 하거나(dmem_error 신호 활성화), 잘못된 명령어를 실행하는 경우(instr_valid 신호 비활성화) 프로그램 실행을 즉시 중단시켜야 한다. 이를 판단하기 위해선 imem_error, dmem_error, instr_valid 등의 값도 알아야 한다.결국, CPU 내부 컨트롤 신호 발생 장치는 새로운 사이클에 진입하여 명령어 하나를 읽는 순간 몇 가지 입력을 바탕으로 아래와 같이 모든 컨트롤 신호들을 결정하고, 이를 바탕으로 해당 사이클 내에 그 명령어의 실행을 각 단계에서 적절히 처리하게 된다.
3-3. Y86-64 각 명령어의 실행 흐름 예시
4. SEQ 단계별 컨트롤 로직
4-1. Fetch
① Predefined Block | 역할 |
PC (Program Counter) | 다음에 실행할 명령어의 주소를 저장하는 레지스터 |
명령어 메모리 (Instruction Memory) | 프로그램 명령어를 저장하는 메모리 (일단 10바이트만큼 읽음) |
Split | 명령어의 첫 번째 바이트를 icode와 ifun으로 분리 |
Align | 명령어의 나머지 비트 배열에서 rA, rB, valC를 추출 |
PC increment | PC 값에 명령어의 길이를 더한 값을 계산 |
② Control Logic | 역할 |
icode | 명령어의 종류를 나타내는 명령어 코드(Instruction Code)를 결정한다. |
ifun | 명령어의 구체적 기능을 구별하는 명령어 함수(Instruction Function)를 결정한다. |
need_valC | 명령어 비트 배열에 valC가 존재하는지 판단한다. |
need_regids | 명령어 비트 배열에 레지스터 바이트가 존재하는지 판단한다. |
instr_valid | 유효한 명령어를 실행하는지 판단한다. |
참고로 imem_error와 instr_valid는 명령어의 실행 상태를 판단할 때 필요한 컨트롤 신호이다. imem_error는 명령어 메모리에 잘못된 주소로 접근했을 때 활성화되며, instr_valid는 유효한 명령어를 실행했을 때만 활성화된다.
4-2. Decode (+ Write back)
① Predefined Block | 역할 |
레지스터 파일 (Register File) | 읽기 포트: A, B
쓰기 포트: E, M
※ 주소로 0xF(=15)를 입력하면 어떠한 레지스터에도 접근하지 않음을 의미 |
② Control Logic | 역할 |
srcA, srcB | 읽을 레지스터의 번호(주소)를 결정한다. |
dstE, dstM | 쓸 레지스터의 번호(주소)를 결정한다. |
Cnd는 조건 만족 여부에 따라 데이터를 이동시킬지 말지를 결정하는 컨트롤 신호로, Execute 단계에서 계산이 된다. 조건이 만족되면 Cnd 의 값이 1로 계산되므로 rB가 dstE에 입력되도록 하고, 반대로 조건이 만족되지 않으면 Cnd의 값이 0으로 계산되어 RNONE(= 0xF)이 dstE에 입력되도록 한다. dstE에 0xF가 입력되면 데이터 이동이 일어나지 않게 되는 원리를 응용한 것이다.
4-3. Execute
① Predefined Block | 역할 |
ALU (Arithmetci Logic Unit) | 네 종류의 산술/논리 연산 수행컨디션 코드 레지스터의 값 셋팅 |
CC (Condition Code) | 산술/논리 연산 결과값에 따라 셋팅되는 컨디션 코드 레지스터 |
cond | 조건 만족 여부를 판단 (조건 분기/이동 시 필요) |
② Control Logic | 역할 |
aluA | ALU에 첫 번째 입력(A)으로 들어갈 값을 결정한다. |
aluB | ALU에 두 번째 입력(B)으로 들어갈 값을 결정한다. |
alufun | ALU가 어떠한 연산을 수행할지 결정한다. (ADD, SUB, AND, XOR) |
set_cc | 컨디션 코드 레지스터의 값이 셋팅되어야 하는 상황인지 판단한다. |
4-4. Memory
① Predefined Block | 역할 |
데이터 메모리 (Data Memory) | 프로그램 데이터를 저장하는 메모리 |
② Control Logic | 역할 |
mem_read | 데이터 메모리로부터 값을 읽을지 판단한다. |
mem_write | 데이터 메모리에 값을 쓸지 판단한다. |
mem_addr | 접근할 데이터 메모리의 주소를 결정한다. |
mem_data | 데이터 메모리에 쓸 값을 결정한다. |
Stat | 명령어 실행 상태를 판단한다. (AOK, ADR, INS, HLT) |
4-5. PC Update
① Predefined Block | 역할 |
PC (Program Counter) | 다음에 실행할 명령어의 주소를 저장하는 레지스터 |
② Control Logic | 역할 |
new_pc | 다음 PC 값을 계산 |
5. SEQ Summary
5-1. 구현 방식 및 한계점
SEQ 구현 방식의 핵심은 일련의 단계들을 거치는 하나의 방식으로 모든 명령어의 실행을 똑같이 처리한다는 것이다. 하지만 구현 방식이 단순한 만큼, 성능은 다른 구현 방식에 비해 현저히 떨어진다. 왜 그런 것일까?
모든 명령어들의 실행을 하나의 일관된 방식으로 처리하려면, 한 사이클 내에 각 단계들에 해당하는 하드웨어들이 전부 사용된다는 가정하에 클락의 주기가 설계되어야 한다. 예를 들어, 하드웨어로 A, B, C, D가 있다고 할 때, 명령어 I1은 A, B를 사용하고, 명령어 I2는 B, C, D를 사용하고, 명령어 I3은 A, B, C, D를 사용한다고 해보자. 그러면 I3 때문에라도 한 사이클 내에 A, B, C, D를 모두 안정적으로 처리할 수 있을 만큼 클락의 주기가 충분히 길어야 한다. I1과 I2 입장에서는 그렇게까지 클락의 주기가 길 필요가 없음에도 불구하고 말이다. 그래서 SEQ 방식은 한 사이클 내에 실제로 하드웨어들이 의미 있게 사용되는 시간적 비율이 작고, 전체적인 처리 속도도 느릴 수밖에 없다.
5-2. 동작 예시
마지막으로 SEQ 구현 방식에서 명령어의 실행에 따라 상태에 해당하는 PC, 레지스터, 메모리, 컨디션 코드 등이 어떻게 변화해 가는지 가시적으로 한 번 살펴보자. 실시간으로 출력이 입력에 따라 변화하는 CLC와 달리 상태에 해당하는 장치들은 클락의 Rising-edge 때만 값이 변경된다는 것과, 한 명령어의 실행은 한 사이클에 처리가 된다는 사실을 염두에 두고 다음 그림들을 차근차근 따라가 보자.