컴퓨터 구조 운영체제 네트워크 자료구조 알고리즘 데이터베이스 내 학습
arch · 03

명령어 사이클

사이클명령어CPU

명령어 사이클은 CPU가 하나의 명령어를 처리하기 위해 반복하는 일련의 단계입니다. 인출(Fetch) → 해독(Decode) → 실행(Execute)의 흐름이 끊임없이 반복되며, 컴퓨터가 프로그램을 실행하는 근본 동작 원리입니다.

1단계

인출 (Fetch)

PC가 가리키는 메모리 주소에서 명령어를 가져옵니다. MAR → 메모리 → MDR → IR 순서로 전달되고, PC는 자동으로 증가합니다.

2단계

해독 (Decode)

제어 유닛(CU)이 IR에 저장된 명령어를 분석합니다. 연산 코드(opcode)와 피연산자(operand)를 분리해 수행할 동작을 결정합니다.

3단계

실행 (Execute)

ALU가 산술·논리 연산을 수행하거나, 메모리 접근(LOAD/STORE)이 일어납니다. 결과는 레지스터 또는 메모리에 저장됩니다.

핵심 레지스터

PC / MAR / MDR / IR

PC는 다음 명령어 주소, MAR은 접근할 주소, MDR은 전송 데이터, IR은 현재 실행 중인 명령어를 보관합니다.

PC 자동 증가 — Fetch 단계에서 명령어를 읽는 즉시 PC가 다음 명령어를 가리키도록 증가합니다(이 시각화는 명령어 1개를 주소 1칸으로 보는 단순 모델이라 +1). 점프(JMP) 명령어는 이 값을 강제로 덮어씁니다.
인터럽트 체크 — Execute 완료 후 인터럽트 요청이 있으면 현재 상태를 스택에 저장하고 ISR(인터럽트 서비스 루틴)을 실행합니다.
파이프라이닝과의 관계 — 단순 사이클은 한 번에 하나의 명령어만 처리합니다. 파이프라이닝은 이 단계들을 겹쳐서 동시에 여러 명령어를 처리합니다.
MAR/MDR의 역할 — CPU 내부 버스와 외부 메모리 버스를 연결하는 인터페이스 레지스터입니다. 모든 메모리 접근은 이 레지스터를 경유합니다. 속도 차이를 완충하는 역할은 캐시(Cache)가 담당합니다.

명령어 하나에 걸리는 시간

프로그램의 실행 시간은 실행한 명령어 수 × 명령어당 평균 클럭 수(CPI) × 클럭 한 주기의 길이로 나뉘어 설명됩니다. 명령어 사이클은 이 중 CPI를 이해하는 출발점입니다. 레지스터끼리 더하는 명령어와 메모리에서 값을 읽어 오는 명령어는 같은 세 단계를 거쳐도 걸리는 클럭 수가 다르고, 이 차이를 줄이려는 노력이 파이프라이닝, 캐시, 분기 예측, 비순차 실행으로 이어집니다.

자주 하는 오해

PC는 항상 1씩 증가한다 — 이 시각화는 명령어 하나가 주소 한 칸을 차지하는 단순 모델이라 +1로 그립니다. 실제로 PC는 방금 가져온 명령어의 길이만큼 증가하며, x86처럼 명령어 길이가 제각각인 구조에서는 명령어마다 증가량이 다릅니다.
Fetch → Decode → Execute 세 단계가 정해진 표준이다 — 설명을 위해 단순화한 구분입니다. 교재에 따라 피연산자 인출(Operand Fetch)과 결과 저장(Write-back)을 따로 떼어 4~5단계로 설명하고, 고전적인 5단계 파이프라인은 IF, ID, EX, MEM, WB로 나눕니다.
모든 명령어가 같은 시간이 걸린다 — 레지스터끼리의 연산은 짧고, 메모리를 읽는 LOAD는 메모리 접근이 더해져 더 오래 걸립니다. 원하는 데이터가 캐시에 없으면 훨씬 더 걸리기도 합니다.
인터럽트는 명령어 실행 도중 아무 때나 끼어든다 — 일반적으로 명령어와 명령어 사이에서 확인해, 저장되는 상태가 '앞 명령어까지 끝나고 다음 명령어는 시작 전'이 되게 합니다. 이렇게 순차 실행했을 때와 같은 상태를 저장하는 인터럽트를 정밀 인터럽트(precise interrupt)라고 부릅니다.

면접에서 이렇게 물어요

면접 질문

명령어 사이클의 단계를 설명해 보세요.

Fetch에서 PC가 가리키는 주소를 MAR에 넣어 메모리에서 명령어를 읽어 MDR을 거쳐 IR에 담고 PC를 다음 명령어로 옮깁니다. Decode에서 CU가 opcode와 피연산자를 해석하고, Execute에서 ALU 연산이나 메모리 접근을 수행합니다.

면접 질문

분기·점프 명령어는 사이클에 어떤 영향을 주나요?

실행 단계에서 PC를 새 주소로 덮어씁니다. 그러면 다음 Fetch가 그 주소에서 시작합니다. 파이프라인 CPU에서는 이 때문에 이미 가져온 명령어를 버려야 할 수 있어 분기 예측이 필요합니다.

면접 질문

파이프라이닝은 명령어 사이클을 어떻게 바꾸나요?

명령어 하나가 끝나는 데 걸리는 시간이 줄지는 않지만, 단계를 겹쳐 여러 명령어를 동시에 진행하므로 단위 시간당 끝나는 명령어 수(처리량)가 늘어납니다.

면접 질문

CPI와 IPC는 무엇인가요?

CPI는 명령어 하나에 평균 몇 클럭이 걸리는지, IPC는 클럭 한 번에 평균 몇 개의 명령어가 끝나는지입니다. 서로 역수 관계이며, 같은 프로그램을 같은 클럭 속도로 돌릴 때는 IPC가 높은 CPU가 더 빠릅니다.

실제 시스템에서는

고전적인 5단계 파이프라인 — RISC 교재의 표준 예는 IF, ID, EX, MEM, WB 다섯 단계로 명령어 사이클을 나눕니다. 이 사이트의 파이프라이닝 토픽이 같은 구성을 사용합니다.
x86의 내부 변환 — 많은 x86 CPU는 길이와 동작이 제각각인 x86 명령어를 해독 단계에서 더 단순한 내부 명령(마이크로연산)으로 나누어 실행합니다. 겉으로는 CISC이지만 안쪽은 RISC처럼 다루는 셈입니다.
정밀 인터럽트 — 파이프라인 CPU에서도 인터럽트가 났을 때 순차 실행과 같은 상태를 저장해야 합니다. Smith와 Pleszkun의 1988년 IEEE 논문은 이를 위한 다섯 가지 해법을 정리한 대표적인 자료입니다.

함께 보면 좋은 토픽

CPU 구성 요소 — CPU 구성 요소 — 명령어 사이클에 등장하는 레지스터와 ALU, CU의 역할을 봅니다.
파이프라이닝 — 파이프라이닝 — 사이클의 단계를 겹쳐 처리량을 높이는 방법과 해저드를 봅니다.
인터럽트 — 인터럽트 — Execute 뒤에 인터럽트를 확인하고 ISR로 넘어가는 과정을 봅니다.
분기 예측 — 분기 예측 — 분기 명령어의 결과를 기다리지 않고 다음 Fetch를 이어 가는 방법입니다.
비순차 실행 — 비순차 실행 — Execute 단계를 기다림 없이 채우는 방법입니다.

참고 자료

C — 명령어 사이클 시뮬레이션

// 명령어 사이클: Fetch → Decode → Execute 반복

typedef struct {
    int memory[256];  // 명령어 + 데이터 동일 메모리 (폰 노이만)
    int PC;           // Program Counter  — 다음 명령어 주소
    int MAR;          // Memory Address Register
    int MDR;          // Memory Data Register
    int IR;           // Instruction Register
    int ACC;          // Accumulator
} CPU;

void run(CPU *cpu) {
    while (1) {
        /* ① FETCH — PC → MAR, MEM[MAR] → MDR → IR, PC++ */
        cpu->MAR = cpu->PC;
        cpu->MDR = cpu->memory[cpu->MAR];
        cpu->IR  = cpu->MDR;
        cpu->PC++;

        /* ② DECODE — opcode(상위 8비트) / operand(하위 8비트) 분리 */
        int opcode  = (cpu->IR >> 8) & 0xFF;
        int operand =  cpu->IR & 0xFF;

        /* ③ EXECUTE */
        if      (opcode == 0x01) cpu->ACC  = cpu->memory[operand]; // LOAD
        else if (opcode == 0x02) cpu->memory[operand] = cpu->ACC;  // STORE
        else if (opcode == 0x03) cpu->ACC += cpu->memory[operand]; // ADD
        else if (opcode == 0x04) cpu->ACC -= cpu->memory[operand]; // SUB
        else if (opcode == 0xFF) break;                            // HALT
    }
}

MAR/MDR은 CPU-메모리 간 속도 차이를 흡수하는 버퍼 레지스터입니다. PC는 Fetch마다 자동 증가하며, 점프 명령어만 이 값을 강제로 바꿉니다.

Python — 인터럽트 포함 확장 사이클

# Fetch → Decode → Execute → Interrupt Check 전체 사이클

def run(cpu):
    while True:
        # ① FETCH
        cpu['MAR'] = cpu['PC']
        cpu['MDR'] = cpu['mem'][cpu['MAR']]
        cpu['IR']  = cpu['MDR']
        cpu['PC'] += 1

        # ② DECODE
        opcode  = (cpu['IR'] >> 8) & 0xFF
        operand =  cpu['IR'] & 0xFF

        # ③ EXECUTE
        if   opcode == 0x01: cpu['ACC']  = cpu['mem'][operand]   # LOAD
        elif opcode == 0x02: cpu['mem'][operand] = cpu['ACC']   # STORE
        elif opcode == 0x03: cpu['ACC'] += cpu['mem'][operand]  # ADD
        elif opcode == 0xFF: break                                 # HALT

        # ④ INTERRUPT CHECK — 사이클과 사이클 사이에서만 처리
        if interrupt_pending():
            stack.push(cpu['PC'])   # 현재 PC 저장
            cpu['PC'] = isr_address  # ISR로 점프

인터럽트는 반드시 Execute 완료 후에 체크합니다. 명령어 실행 도중에는 처리하지 않아야 레지스터 상태의 일관성이 보장됩니다. 실제로는 PC(EIP)뿐 아니라 범용 레지스터 전체, EFLAGS 등을 스택에 저장해야 ISR 종료 후 원래 상태로 완전히 복귀할 수 있습니다. 위 코드는 흐름 설명을 위해 PC 저장만 표현한 단순화 예시입니다.

Q1

명령어 사이클의 기본 순서는?

먼저 명령어를 가져와야(Fetch) 해석(Decode)할 수 있습니다.
CPU는 Fetch(인출)→Decode(해독)→Execute(실행) 순서를 반복하며 프로그램을 실행합니다.

Q2

Fetch 단계에서 사용되는 레지스터는?

다음에 실행할 명령어의 주소를 담고 있는 레지스터입니다.
PC가 가리키는 메모리 주소에서 명령어를 가져오는 것이 Fetch 단계이며, 이후 PC는 다음 명령어를 가리키도록 갱신됩니다.

Q3

Decode 단계에서 하는 일은?

명령어 비트 패턴을 보고 무엇을 할지 알아내는 단계입니다.
Decode 단계는 Fetch한 명령어(opcode, operand)를 해석해 어떤 연산과 어떤 데이터가 필요한지 판별합니다.