🟣 32비트 숫자 하나가 회로를 통과한다
앞 장에서 명령어가 32비트 숫자로 인코딩되어 메모리에 올라갔다. 이제 회로가 그 숫자를 한 줄씩 꺼내 실행할 차례다. 문제를 뒤집어 보면 이렇다. add $t0, $t1, $t2라는 기계어 한 줄이 회로에 들어왔을 때, 어떤 부품들이 어떤 순서로 켜져야 실제로 덧셈이 일어나고 결과가 레지스터에 남는가.
명령어를 실행하는 과정은 어떤 명령어든 대체로 같은 골격을 따른다. 먼저 PC가 가리키는 곳에서 명령어를 꺼내고(fetch), 비트를 잘라 무슨 명령어인지 해독하고(decode), 필요한 레지스터 값을 읽어 계산하고(execute), 메모리를 오가야 하면 접근하고, 마지막으로 결과를 레지스터에 되쓴다(write back). 이 골격을 실제 부품으로 그려 낸 것이 datapath다. 이 장은 그 datapath를 하나씩 세우고, 명령어마다 이 통로를 어떻게 다르게 쓰는지를 본다.
🟣 datapath를 이루는 부품들
datapath는 몇 개의 표준 부품으로 이뤄진다. 각각이 하는 일은 단순하고, 이들이 배선으로 이어져 명령어 하나를 처리한다.
가장 앞에 PC가 있고, 그 주소로 명령어 메모리에서 32비트 명령어를 읽는다. 읽은 명령어는 비트가 잘려 각 부품으로 흩어진다. 레지스터 번호들은 레지스터 파일로 간다. 레지스터 파일은 32개 레지스터의 묶음인데, 읽을 레지스터 두 개의 번호를 주면 그 값을 동시에 내주고, 쓸 레지스터 번호와 값을 주면 그 자리에 써 넣는다.
읽어 온 두 값은 ALU(산술논리연산장치)로 들어간다. ALU는 덧셈, 뺄셈, 논리곱, 비교 같은 실제 계산을 담당하는 회로다. lw·sw라면 ALU가 주소를 계산하고, 그 주소로 데이터 메모리에 접근한다. 명령어 메모리와 데이터 메모리를 따로 그리는 것이 어색해 보이지만, 이 분리가 다음 장 파이프라인에서 왜 중요한지 곧 드러난다.
여기에 보조 부품 둘이 낀다. 16비트 상수를 32비트로 늘리는 부호 확장기, 그리고 여러 입력 중 하나를 골라 내보내는 멀티플렉서다. 멀티플렉서가 특히 결정적이다. 예를 들어 ALU의 두 번째 입력이 레지스터 값일 때도 있고 상수일 때도 있는데, 둘 중 무엇을 흘려보낼지 멀티플렉서가 고른다. 명령어마다 통로가 달라지는 지점은 거의 다 이 멀티플렉서에서 갈린다.
🟣 통로는 하나, 쓰는 법은 명령어마다 다르다
같은 datapath를 두고 명령어들은 서로 다른 경로를 탄다. 네 가지 대표 명령어가 이 통로를 어떻게 다르게 쓰는지 보면 구조가 손에 잡힌다.
R형식 add는 레지스터 파일에서 두 값을 읽어 ALU로 더하고, 데이터 메모리는 건너뛴 채 결과를 곧장 레지스터에 되쓴다. lw(load)는 ALU로 주소를 계산한 뒤 데이터 메모리에서 값을 읽어 레지스터에 넣는다. 같은 ALU를 R형식은 계산에, load는 주소 산출에 쓰는 점이 재미있다. sw(store)는 반대로 레지스터 값을 데이터 메모리에 쓰기만 하고 되쓰기는 없다. beq는 ALU로 두 값을 빼서 같은지 보고, 같으면 PC를 분기 목적지로 바꾼다.
명령어마다 다른 것은 결국 세 가지다. 데이터 메모리를 읽거나 쓰는가, 레지스터에 결과를 되쓰는가, ALU의 입력으로 상수를 쓰는가. 하드웨어는 그대로 두고 몇 개의 스위치만 바꿔 명령어별 동작을 만들어 낸다. 이 스위치를 누가 조작하느냐가 다음 이야기다.
🟣 제어: opcode를 보고 스위치를 세팅한다
datapath의 멀티플렉서와 메모리 읽기/쓰기 신호를 통틀어 제어 신호라고 부른다. 이 신호들을 만들어 내는 회로가 제어 유닛이다. 제어 유닛이 하는 일은 단순하다. 명령어의 opcode를 입력으로 받아, 그 명령어에 맞는 스위치 조합을 출력한다.
대표적인 제어 신호 몇 개만 봐도 그림이 선다. RegWrite는 레지스터에 되쓸지 여부다. add와 lw는 켜고, sw와 beq는 끈다. ALUSrc는 ALU의 둘째 입력으로 레지스터를 쓸지 상수를 쓸지 고른다. add는 레지스터, lw는 상수 쪽으로 멀티플렉서를 민다. MemRead·MemWrite는 데이터 메모리 접근이고, Branch는 분기 여부다. 제어 유닛은 opcode 하나를 보고 이 신호들을 한꺼번에 세팅한다.
| 명령어 | RegWrite | ALUSrc | MemRead | MemWrite | Branch |
|---|---|---|---|---|---|
| add (R) | 1 | 0 (레지스터) | 0 | 0 | 0 |
| lw | 1 | 1 (상수) | 1 | 0 | 0 |
| sw | 0 | 1 (상수) | 0 | 1 | 0 |
| beq | 0 | 0 (레지스터) | 0 | 0 | 1 |
여기서 앞 장의 명령어 형식이 왜 그렇게 생겼는지가 이어진다. opcode를 맨 앞 6비트에 고정으로 둔 덕에, 제어 유닛은 나머지 비트를 해석하기 전에 곧바로 스위치를 정할 수 있다. 명령어 인코딩과 제어 회로가 같은 설계 의도를 공유한다는 것을, 이 표를 직접 채워 보며 알았다.
🟣 단일 사이클의 한계
지금까지 그린 방식은 명령어 하나를 한 클럭 사이클에 통째로 끝낸다. 단일 사이클 방식이다. 이해하기엔 좋지만 성능에서 큰 약점이 있다.
클럭 주기는 가장 오래 걸리는 명령어에 맞춰 정해진다. 명령어마다 통과하는 경로 길이가 다른데, lw는 명령어 메모리, 레지스터 파일, ALU, 데이터 메모리, 되쓰기를 전부 거쳐 가장 길다. 반면 add는 데이터 메모리를 건너뛰어 훨씬 짧다. 그런데 모든 명령어가 한 사이클에 끝나야 하니, 클럭은 가장 느린 lw 기준으로 늘어진다. 짧게 끝날 add도 남은 시간 동안 그냥 기다린다.
부품이 노는 것도 낭비다. 한 명령어가 데이터 메모리를 쓰는 그 순간에도 ALU와 명령어 메모리는 이미 제 일을 마치고 놀고 있다. 한 명령어가 사이클 내내 datapath 전체를 독점하는 이 구조가 성능의 천장이다. 이 낭비를 없애려면, 여러 명령어가 datapath의 서로 다른 구간을 동시에 쓰게 하면 된다. 그것이 파이프라인이다.
🟣 정리
- 명령어 실행은 꺼내기, 해독, 계산, 메모리 접근, 되쓰기의 골격을 따른다. datapath는 이 골격을 실제 부품으로 그린 것이다.
- PC, 명령어 메모리, 레지스터 파일, ALU, 데이터 메모리가 배선으로 이어지고, 멀티플렉서가 명령어별로 경로를 가른다.
- 같은 datapath를 R형식은 계산에, load는 주소 산출에 쓰는 식으로 명령어마다 다르게 통과한다.
- 제어 유닛은 opcode를 보고 RegWrite·ALUSrc 같은 스위치를 한꺼번에 세팅한다. 명령어 인코딩과 제어 설계가 의도를 공유한다.
- 단일 사이클은 클럭이 가장 느린 명령어에 묶이고 부품이 놀아 낭비가 크다.
면접에서 datapath를 물으면 부품 이름을 나열하기보다, lw 한 줄이 각 부품을 어떻게 지나는지 경로로 설명하는 편이 깊이가 산다. 제어 신호가 결국 멀티플렉서의 선택이라는 점을 잡으면, 복잡해 보이는 제어 표가 스위치 조합으로 단순해진다.
단일 사이클의 낭비를 확인했으니, 다음 장에서는 여러 명령어를 겹쳐 흘려보내는 파이프라인으로 넘어간다. 처리량이 크게 오르지만, 겹쳐 흐르기 때문에 앞뒤 명령어가 서로 방해하는 해저드라는 새 문제가 따라온다.