본문 바로가기
TYLER SONGBlog
블로그 목록
CS학습

Execute: 명령어 사이클의 마지막 관문, 실제 연산이 일어나는 단계

Execute는 Fetch-Decode-Execute로 이어지는 명령어 사이클(instruction cycle)에서 실제 연산이 수행되는 단계다. Decode 단계에서 해석된 제어 신호에 따라 ALU 연산, 메모리 접근, 레지스터 갱신이 이 단계에서 이루어진다. 파이프라이닝(pipelining) 구조에서는 Execute 단계가 데이터 해저드(data haz

송민성6분 읽기

1. 개념

Execute는 CPU가 명령어를 처리하는 명령어 사이클(instruction cycle)의 세 번째 단계다. 전체 흐름은 보통 다음과 같이 나뉜다.

  1. Fetch - 메모리에서 명령어를 가져온다.
  2. Decode - 가져온 명령어를 해석해서 어떤 연산인지, 어떤 레지스터/메모리를 사용하는지 판단한다.
  3. Execute - 실제로 연산을 수행한다.
  4. (필요시) Write-back - 결과를 레지스터나 메모리에 저장한다.

Execute 단계에서는 ALU(Arithmetic Logic Unit, 산술논리연산장치)가 실제 덧셈, 뺄셈, 논리 연산, 주소 계산 등을 수행하거나, 메모리 접근(load/store) 명령이면 주소 버스에 실제 주소를 실어 메모리 읽기/쓰기를 시작한다.

2. 왜 사용하는가

명령어를 Fetch하고 Decode만 하면 "무엇을 할지"만 알게 될 뿐, 실제로 값이 바뀌거나 계산이 이루어지지 않는다. Execute 단계가 있어야 프로그램이 실제로 상태를 변화시키는 부작용(side effect)을 만들어낸다. 즉 Fetch/Decode는 준비 단계이고 Execute는 실제 작업이 벌어지는 단계다.

이 단계를 별도로 분리하는 이유는 하드웨어 설계 관점에서 각 단계를 독립된 회로 블록으로 나눠 파이프라이닝을 가능하게 하기 위해서다. 단계를 나누지 않으면 명령어 하나를 처리하는 동안 CPU의 다른 회로들이 유휴 상태로 남는다.

3. 동작 원리

Decode 단계에서 만들어진 제어 신호(control signal)가 Execute 단계의 데이터패스(datapath)를 구성한다. 예를 들어 ADD R1, R2, R3 명령어라면:

  1. Decode에서 "ALU는 덧셈 연산을 수행하고, 입력은 R2와 R3, 출력은 R1"이라는 제어 신호가 만들어진다.
  2. Execute 단계에서 레지스터 파일(register file)에서 R2, R3 값을 읽어 ALU 입력 포트에 전달한다.
  3. ALU가 제어 신호에 따라 덧셈 연산을 수행한다.
  4. 결과값이 임시 레지스터나 버스에 실려 다음 단계(Write-back)로 전달된다.

Load/Store 명령어의 경우 Execute 단계에서는 ALU가 유효 주소(effective address)를 계산하는 역할을 한다. 예를 들어 LOAD R1, [R2+4]라면 ALU가 R2 + 4를 계산해서 실제 메모리 접근 주소를 만든다. 실제 메모리 접근은 별도의 Memory Access 단계에서 이루어지는 구조도 많다(MIPS 5단계 파이프라인 기준: Fetch-Decode-Execute-Memory-Writeback).

RTL(Register Transfer Level) 표기로는 다음처럼 표현한다.

text
Execute(ADD): ALU_out ← R[rs] + R[rt]
Execute(LOAD): ALU_out ← R[rs] + offset

4. 코드 예제

간단한 명령어 사이클을 소프트웨어로 시뮬레이션한 예제다. Fetch-Decode-Execute 흐름과 Execute 단계에서 ALU 연산이 실제로 일어나는 부분을 명확히 분리했다.

python
from dataclasses import dataclass @dataclass class Instruction: opcode: str rd: str rs: str rt: str class SimpleCPU: def __init__(self): self.registers = {"R0": 0, "R1": 0, "R2": 0, "R3": 5, "R4": 10} self.memory = [ Instruction("ADD", "R1", "R3", "R4"), # R1 = R3 + R4 Instruction("SUB", "R2", "R4", "R3"), # R2 = R4 - R3 ] self.pc = 0 # program counter def fetch(self) -> Instruction: instr = self.memory[self.pc] self.pc += 1 return instr def decode(self, instr: Instruction): # 제어 신호 결정: 어떤 ALU 연산을 쓸지, 입력 레지스터가 무엇인지 control_signal = instr.opcode operand_a = self.registers[instr.rs] operand_b = self.registers[instr.rt] return control_signal, operand_a, operand_b, instr.rd def execute(self, control_signal, operand_a, operand_b): # ALU 연산이 실제로 일어나는 지점 if control_signal == "ADD": return operand_a + operand_b elif control_signal == "SUB": return operand_a - operand_b else: raise ValueError(f"지원하지 않는 연산: {control_signal}") def write_back(self, rd, result): self.registers[rd] = result def run(self): while self.pc < len(self.memory): instr = self.fetch() control_signal, a, b, rd = self.decode(instr) result = self.execute(control_signal, a, b) self.write_back(rd, result) print(f"{instr.opcode} 실행 -> {rd} = {result}") cpu = SimpleCPU() cpu.run() print(cpu.registers)

실행 결과:

text
ADD 실행 -> R1 = 15
SUB 실행 -> R2 = 5
{'R0': 0, 'R1': 15, 'R2': 5, 'R3': 5, 'R4': 10}

execute 메서드가 실제 ALU 역할을 하는 부분이며, 이 함수 호출 이전까지는 어떤 상태 변화도 일어나지 않는다는 점이 핵심이다.

5. 시간 복잡도 또는 성능 특성

Execute 단계 자체는 알고리즘적 시간복잡도로 표현하기보다 클럭 사이클(clock cycle) 수로 측정한다.

  • 단순 정수 ALU 연산(덧셈, 뺄셈, 논리 연산)은 대부분 1클럭 사이클 내에 완료된다.
  • 정수 나눗셈이나 부동소수점 연산은 여러 클럭 사이클이 걸릴 수 있다. 예를 들어 x86 계열에서 정수 나눗셈은 연산 종류와 마이크로아키텍처에 따라 수십 클럭 사이클이 소요될 수 있다(정확한 사이클 수는 CPU 세대마다 다르므로 특정 수치를 일반화하기는 어렵다).
  • 파이프라이닝된 CPU에서는 Execute 단계가 여러 명령어의 처리 흐름 중 하나의 슬롯을 차지하며, 이론적으로는 매 클럭마다 새로운 명령어가 Execute 단계에 진입할 수 있어 처리율(throughput)이 1 IPC(Instructions Per Cycle)에 근접할 수 있다. 다만 데이터 해저드나 분기 예측 실패가 발생하면 파이프라인 스톨(stall)이나 버블(bubble)이 생겨 실질 IPC가 낮아진다.

6. 실무 사용 사례

  • 컴파일러 최적화 이해: 컴파일러가 명령어 스케줄링(instruction scheduling)을 할 때, Execute 단계의 지연시간(latency)을 고려해 독립적인 연산끼리 재배치한다. 이를 이해하면 왜 특정 코드가 CPU 파이프라인에 유리한지 판단할 수 있다.
  • 성능 프로파일링: perf 같은 도구로 IPC를 측정할 때, 낮은 IPC는 Execute 단계에서의 스톨(예: 캐시 미스로 인한 대기, 데이터 의존성)을 의심하는 근거가 된다.
  • SIMD/벡터 연산 활용: Execute 단계의 ALU를 벡터 유닛으로 확장한 SIMD(Single Instruction Multiple Data) 명령어를 활용하면 한 번의 Execute로 여러 데이터를 동시에 처리해 처리율을 높일 수 있다.

7. 주의할 점

  • 데이터 해저드(data hazard): 파이프라이닝된 CPU에서 한 명령어의 Execute 결과가 다음 명령어의 Execute 입력으로 즉시 필요한 경우, 결과가 Write-back되기 전에 사용해야 하는 문제가 생긴다. 이를 해결하기 위해 포워딩(forwarding, bypassing) 기법이 사용된다.
  • 분기 예측 실패(branch misprediction): 조건 분기 명령어의 Execute 결과가 나오기 전까지 이후 명령어를 추측해서 미리 Fetch/Decode한 경우, 예측이 틀리면 이미 파이프라인에 들어간 명령어들을 모두 무효화(flush)해야 한다. 이는 상당한 성능 손실로 이어진다.
  • 가변 지연시간 연산: 나눗셈이나 부동소수점 연산처럼 Execute 단계의 소요 시간이 명령어마다 다르면 파이프라인 설계가 복잡해지고, 이를 처리하기 위해 별도의 실행 유닛과 스케줄링 로직이 필요하다.

8. 핵심 정리

Execute는 Decode에서 해석된 제어 신호를 바탕으로 ALU가 실제 연산을 수행하거나 메모리 주소를 계산하는 단계로, 프로그램의 상태를 실질적으로 변화시키는 지점이다. 단순 정수 연산은 대개 1클럭 사이클에 끝나지만 나눗셈이나 부동소수점 연산은 더 오래 걸릴 수 있다. 파이프라이닝 환경에서는 Execute 단계가 데이터 해저드와 분기 예측 실패의 영향을 직접 받는 지점이므로, 포워딩이나 분기 예측 같은 보완 기법과 함께 이해해야 실질적인 CPU 성능 특성을 파악할 수 있다.

© 2026 Tyler Song