도입
우리가 작성한 프로그램은 결국 CPU가 처리하는 명령어들의 흐름으로 실행됩니다. 하지만 CPU는 코드를 한꺼번에 이해하지 않습니다. 현재 어디를 실행해야 하는지 찾고, 그 명령이 무엇을 의미하는지 해석하고, 필요한 연산이나 메모리 접근을 수행하고, 결과를 상태에 반영한 뒤 다시 다음 명령으로 넘어갑니다. 이 반복 구조를 명령어 사이클(Instruction Cycle)이라고 합니다.
교과서에서는 보통 Fetch → Decode → Execute로 요약하고, 더 자세한 설명에서는 Memory Access, Write-back, Interrupt Check까지 확장해 설명합니다. 중요한 점은 명령어 사이클이 단순한 학습용 용어가 아니라, 캐시 미스, 분기 예측 실패, 페이지 폴트, 시스템 콜, 컨텍스트 스위치 같은 실제 성능과 장애의 출발점을 이해하는 틀이라는 점입니다.
필요성
많은 초보자는 CPU를 단순한 계산기처럼 생각하지만, 실제 CPU는 계산뿐 아니라 명령 인출, 해석, 제어 흐름 변경, 메모리 접근, 예외 처리를 함께 담당합니다. 명령어 사이클을 이해하면 왜 같은 코드라도 캐시 친화적인지에 따라 성능이 달라지는지, 왜 분기문이 많은 코드가 느려질 수 있는지, 왜 시스템 콜 뒤에 갑자기 커널 시간이 늘어나는지 같은 질문에 훨씬 정확하게 답할 수 있습니다.
- 컴퓨터구조와 CPU 동작 원리 학습
- 운영체제, 인터럽트, 예외 처리 이해
- 백엔드 성능 최적화와 병목 분석
- 어셈블리, 바이너리, 디버깅 분석
- JIT, GC, 런타임 동작 이해
- 멀티스레드, 락, 캐시 일관성 문제 분석
정의
가장 단순한 설명은 Fetch → Decode → Execute입니다. 하지만 조금 더 정확히 보면 명령어 종류에 따라 유효 주소 계산, 메모리 접근, 쓰기 반영(Write-back)이 추가되고, 각 명령이 끝난 뒤에는 인터럽트 처리 가능 여부도 고려됩니다. 따라서 명령어 사이클은 고정된 3단계 절차라기보다, CPU가 명령을 수행할 때 거치는 표준적인 실행 흐름의 추상화라고 보는 편이 맞습니다.
큰 그림
PC가 다음 명령어 위치를 가리킴
↓
Fetch: 명령어 바이트를 가져옴
↓
Decode: opcode / 오퍼랜드 / 주소지정 방식 해석
↓
(Optional) Indirect / EA 계산: 실제 접근 주소를 확정
↓
Execute: ALU 연산 / 분기 판단 / 주소 계산 / trap 발생
↓
(Optional) Memory Access: load/store 수행
↓
(Optional) Write-back: 결과를 레지스터 파일 등에 반영
↓
Interrupt Check: 인터럽트 처리 여부 확인
↓
다음 명령어로 반복
여기서 핵심 자원은 PC(Program Counter), IR(Instruction Register), 제어 장치(Control Unit), 레지스터 파일(Register File), ALU, 캐시/메모리입니다. 교육용 설명에서는 명령어를 IR에 저장한다고 단순화해 설명하지만, 실제 현대 CPU는 명령 버퍼, 디코드 큐, 마이크로 연산(μops) 같은 더 복잡한 내부 구조를 사용하기도 합니다.
주요 단계
| 단계 | 무엇을 하는가 | 핵심 자원 |
|---|---|---|
| Fetch | 다음 명령어를 I-Cache/메모리에서 가져온다 | PC, I-Cache, 명령 버퍼, IR |
| Decode | opcode, 오퍼랜드, 주소지정 방식을 해석한다 | 제어 장치, 디코더, 레지스터 파일 |
| Indirect / EA | 간접 주소 방식이면 실제 접근 주소를 확정한다 | 주소 생성기, 레지스터, 메모리 |
| Execute | 산술·논리 연산, 비교, 분기 판단, trap 수행 | ALU, 분기 유닛, 제어 로직 |
| Memory Access | load/store가 있으면 실제 데이터 접근을 수행한다 | D-Cache, TLB, 메모리 계층 |
| Write-back | 연산 결과를 목적 레지스터나 상태 레지스터에 반영한다 | 레지스터 파일, 상태 플래그 |
| Interrupt Check | 처리 가능한 인터럽트가 있으면 핸들러로 제어를 넘긴다 | 인터럽트 컨트롤러, PC, 상태 저장 영역 |
1. 인출(Fetch)
CPU는 먼저 PC가 가리키는 주소를 확인합니다. 그 주소의 명령어 바이트를 보통 I-Cache에서 읽고, 필요하면 더 아래 메모리 계층까지 내려가 가져옵니다. 이렇게 가져온 명령어는 교육용 모델에서는 IR에 저장된다고 설명하고, 실제 CPU에서는 명령 버퍼나 디코드 큐로 전달됩니다.
PC 확인
→ 해당 주소의 명령어 바이트 fetch
→ IR 또는 디코드 버퍼에 저장
→ PC를 다음 순차 주소로 갱신
또는 예측된 분기 target으로 갱신
중요한 점은 PC가 항상 단순히 1씩 증가하는 것이 아니라는 사실입니다. ISA에 따라 명령어 길이가 다를 수 있고, 분기·호출·리턴이 있으면 PC는 전혀 다른 주소로 바뀝니다. 즉, Fetch는 단순한 읽기 단계이면서도 동시에 다음 제어 흐름의 출발점을 만드는 단계이기도 합니다.
2. 해독(Decode)
Fetch 단계에서 가져온 것은 아직 단순한 비트열일 뿐입니다. Decode 단계에서는 제어 장치가 이 비트열을 해석해 opcode, 소스/목적 레지스터, 즉시값, 주소지정 방식을 식별합니다. 또한 어떤 연산이 필요한지, 레지스터 파일에서 어떤 값을 읽어야 하는지, 이후 메모리 접근이 필요한지도 이 단계에서 정해집니다.
- 어떤 명령어(opcode)인지
- 어떤 레지스터를 읽고 써야 하는지
- 즉시값(immediate)과 오프셋이 있는지
- 메모리 접근이 필요한지
- ALU, 분기 유닛, load/store 유닛 중 무엇을 사용할지
교육용 모델에서는 Decode를 단순 번역 단계처럼 보지만, 실제로는 이후 실행 경로를 결정하는 제어 신호 생성의 핵심 단계입니다. 고성능 CPU에서는 디코딩 이후 명령을 더 단순한 내부 마이크로 연산으로 쪼개기도 하고, 레지스터 이름 변경(rename) 같은 기법이 뒤따르기도 합니다.
3. 간접(Indirect)과 유효 주소(EA)
교과서에서 말하는 Indirect 단계는 오퍼랜드가 데이터 자체가 아니라 실제 데이터 위치를 가리키는 주소일 때 등장합니다. 이 경우 CPU는 먼저 그 주소를 읽어 최종 접근 위치를 얻고, 그 다음에야 실제 데이터에 접근할 수 있습니다. 이때 확정되는 최종 주소를 유효 주소(EA, Effective Address)라고 부릅니다.
명령어가 A를 가리킴
→ A 위치를 읽어 실제 주소 B를 얻음
→ 최종적으로 B 위치의 데이터를 읽음
다만 현대 load/store ISA 설명에서는 이 단계를 별도의 고정된 단계로 떼기보다, 주소 생성 + 추가 메모리 접근으로 묶어 설명하는 경우가 많습니다. 즉, Indirect는 오늘날에도 유효한 개념이지만, 모든 CPU가 내부적으로 꼭 독립된 “간접 단계”를 이름 붙여 운영하는 것은 아닙니다.
4. 실행(Execute)
Execute 단계에서는 해석된 내용이 실제 동작으로 바뀝니다. 산술 명령이면 ALU가 덧셈, 뺄셈, 비트 연산을 수행하고, 비교 명령이면 상태 플래그를 갱신하며, 분기 명령이면 조건을 검사해 제어 흐름을 바꿉니다. 메모리 명령의 경우 이 단계에서 최종 주소 계산이 이루어지고, 시스템 명령은 trap이나 특권 수준 전환의 출발점이 되기도 합니다.
- ALU 산술·논리 연산
- 비교와 상태 플래그 갱신
- 분기 조건 평가와 PC 변경
- load/store용 주소 계산
- 시스템 콜용 trap 명령 실행
즉, Execute는 단순히 “계산하는 단계”가 아니라, 명령이 요구하는 실제 시스템 상태 변화를 발생시키는 단계라고 보는 편이 더 정확합니다.
5. 메모리 접근과 쓰기 반영(Memory Access / Write-back)
모든 명령이 메모리에 접근하는 것은 아닙니다. 하지만 load/store 명령은 Execute에서 계산한 주소를 바탕으로 실제로 D-Cache, TLB, 메모리 계층에 접근합니다. Load라면 데이터를 읽어 오고, store라면 데이터를 메모리에 기록합니다. 그 뒤 산술 연산 결과나 load 결과는 보통 레지스터 파일에 써서 다음 명령이 사용할 수 있게 합니다. 이 과정을 Write-back이라고 부릅니다.
이 단계는 성능 측면에서도 매우 중요합니다. 캐시 미스가 발생하면 CPU는 계산 능력이 충분해도 데이터를 기다리느라 멈춰 설 수 있고, 공유 데이터에 대한 빈번한 쓰기와 원자적 연산은 캐시 일관성 비용을 키워 지연을 늘릴 수 있습니다.
간단한 예시
예를 들어 ADD R1, R2, R3 같은 명령이 있다고 가정해 보겠습니다. 의미는 R1 = R2 + R3입니다. 이 명령 하나도 CPU 내부에서는 다음과 같이 처리됩니다.
1. Fetch
PC가 가리키는 주소에서 ADD 명령어를 가져온다.
2. Decode
opcode가 ADD임을 해석하고,
소스 레지스터 R2, R3와 목적 레지스터 R1을 식별한다.
3. Execute
ALU가 R2와 R3 값을 더한다.
4. Write-back
결과를 R1에 기록한다.
5. Interrupt Check
처리 가능한 인터럽트가 있으면 핸들러로 진입한다.
만약 LOAD R1, [R2+8] 같은 명령이라면 Execute에서 주소 R2+8을 계산하고, Memory Access에서 실제 데이터를 읽은 뒤, Write-back에서 그 값을 R1에 저장하게 됩니다. 즉, 명령어 종류에 따라 필요한 하위 단계가 조금씩 달라집니다.
인터럽트, 예외, 시스템 콜, 컨텍스트 스위치
교과서에서는 한 명령이 끝날 때 CPU가 Interrupt Check를 수행한다고 설명합니다. 이 관점은 매우 유용합니다. CPU는 현재 명령의 상태를 정확하게 정리한 뒤, 처리 가능한 인터럽트가 있으면 현재 실행 흐름을 잠시 중단하고 핸들러로 넘어갑니다. 반면 예외(exception)는 명령 실행 도중에 동기적으로 발생합니다. 예를 들어 0으로 나누기, 권한 위반, 페이지 폴트는 특정 명령과 직접 연결된 예외입니다.
| 구분 | 발생 방식 | 예시 | 핵심 포인트 |
|---|---|---|---|
| 인터럽트 | 외부 장치나 타이머가 비동기적으로 요청 | 네트워크 패킷 도착, 디스크 I/O 완료, 타이머 틱 | 보통 명령 경계에서 처리하는 모델로 설명 |
| 예외 | 현재 명령 실행 중 동기적으로 발생 | 0으로 나누기, 페이지 폴트, 권한 위반 | 특정 명령과 직접 연결된다 |
| 시스템 콜 | 프로그램이 trap 명령으로 의도적으로 커널 진입 | read, write, epoll_wait, futex | 소프트웨어가 요청한 동기적 커널 전환 |
| 컨텍스트 스위치 | 커널이 다른 스레드/프로세스로 CPU를 넘김 | 타임 슬라이스 만료, 블로킹 I/O, higher-priority wakeup | CPU 단계 자체가 아니라 OS 스케줄링 동작 |
특히 시스템 콜과 컨텍스트 스위치를 같은 것으로 보면 안 됩니다. 시스템 콜은 커널에 요청을 보내는 진입 이벤트이고, 컨텍스트 스위치는 그 과정이나 이후에 스케줄러가 다른 실행 주체로 전환하는 운영체제의 결정입니다. 또한 페이지 폴트는 인터럽트가 아니라 예외라는 점도 꼭 구분해야 합니다.
파이프라이닝과의 관계
기본 교과서 모델은 한 명령어의 Fetch, Decode, Execute가 끝난 뒤 다음 명령으로 넘어가는 것처럼 보입니다. 하지만 현대 CPU는 파이프라이닝을 통해 첫 번째 명령이 Execute에 있을 때 두 번째 명령은 Decode, 세 번째 명령은 Fetch를 동시에 진행할 수 있습니다. 즉, 명령어 사이클은 여전히 기본 개념이지만 실제 구현에서는 여러 명령이 겹쳐서 흐릅니다.
시간축 예시
명령1: Fetch → Decode → Execute → Memory → Write-back
명령2: Fetch → Decode → Execute → Memory → Write-back
명령3: Fetch → Decode → Execute → Memory → Write-back
이 구조 때문에 분기 예측 실패가 발생하면 이미 진행 중이던 후속 단계들을 비워야 하고, 캐시 미스가 나면 여러 명령이 함께 대기할 수 있습니다. 또 현대 CPU는 여기에 superscalar, out-of-order execution, speculative execution을 더해 단일 명령 흐름보다 훨씬 복잡하게 동작합니다.
백엔드 개발자 관점 연결
| 실무 현상 | 명령어 사이클과의 연결 | 왜 중요한가 |
|---|---|---|
| I-Cache 미스 | Fetch 단계가 명령어를 제때 공급하지 못함 | 핫패스 레이아웃과 코드 크기가 지연에 영향 |
| 분기 예측 실패 | Decode/Execute 이후 파이프라인 flush | if/else가 많은 코드, 데이터 의존 분기에서 손해 |
| 포인터 추적 / 캐시 미스 | Indirect/EA 및 Memory Access에서 추가 대기 | 객체 그래프가 깊을수록 메모리 병목 가능성 증가 |
| 페이지 폴트 | Memory Access 중 동기적 예외 발생 | tail latency와 커널 시간 급증의 원인 |
| 시스템 콜 / 블로킹 I/O | trap으로 커널 진입, 이후 스케줄링 가능 | user time, sys time, context switch를 함께 봐야 함 |
| 락 경쟁 / false sharing | Write와 캐시 일관성 트래픽 증가 | 멀티스레드 처리량 저하와 지연 스파이크 유발 |
| JIT / GC | 코드 레이아웃, 메모리 접근 패턴, 스레드 정지에 영향 | 런타임 시스템이 CPU/메모리 흐름을 크게 바꿀 수 있음 |
즉, 백엔드 성능 문제를 볼 때 “CPU 사용률이 높다”는 한 줄만으로는 부족합니다. 어느 단계에서 stall이 발생하는지, 그 stall이 fetch/branch/memory/syscall 중 무엇과 연결되는지까지 내려가야 원인을 제대로 분리할 수 있습니다.
자주 하는 오해
- 명령어 사이클은 클록 사이클과 같다고 생각함 → 한 명령이 여러 클록에 걸릴 수 있고, 여러 명령이 동시에 겹쳐 실행되기도 합니다.
- PC는 항상 1씩 증가한다고 생각함 → ISA와 명령 길이, 분기·호출·리턴에 따라 달라집니다.
- 모든 명령이 Write-back을 가진다고 생각함 → 분기나 store처럼 레지스터 write-back이 없는 명령도 많습니다.
- 인터럽트와 예외는 같은 것이라고 생각함 → 인터럽트는 주로 비동기 외부 이벤트, 예외는 현재 명령과 연결된 동기적 사건입니다.
- 컨텍스트 스위치는 CPU 단계다고 생각함 → 이는 인터럽트/시스템 콜 이후 커널이 수행하는 스케줄링 동작입니다.
- Indirect 단계는 모든 현대 CPU에서 항상 독립적으로 존재한다고 생각함 → 교과서적 설명에 가깝고, 실제 구현은 주소 생성과 추가 memory access로 묶이는 경우가 많습니다.
- 성능 문제는 결국 계산량 문제라고 생각함 → 실제 서버 병목은 메모리 대기, 캐시 미스, 분기 실패, 커널 전환에서 더 자주 나타납니다.
공부 루틴
- PC, IR, 레지스터 파일, ALU, 캐시의 역할부터 먼저 잡는다.
- Fetch → Decode → Execute 3단계 요약 모델을 이해한다.
- Memory Access, Write-back, Interrupt Check를 추가해 확장 모델로 넓힌다.
- ADD, LOAD, BRANCH, SYSCALL 같은 간단한 명령을 예시로 상태 변화를 따라간다.
- 파이프라이닝과 분기 예측을 연결해 현대 CPU 관점으로 확장한다.
- 시스템 콜, 페이지 폴트, 컨텍스트 스위치까지 묶어 소프트웨어-하드웨어 경계를 함께 본다.
디버깅과 분석 포인트
요약
- ✅ 명령어 사이클은 Fetch, Decode, Execute를 중심으로 필요에 따라 Memory Access, Write-back, Interrupt Check까지 확장해 이해한다.
- ✅ Fetch에서는 PC를 기준으로 명령을 가져오고, Decode에서는 opcode와 오퍼랜드와 주소지정 방식을 해석한다.
- ✅ Execute에서는 ALU 연산, 분기 판단, 주소 계산, trap 발생 같은 실제 상태 변화가 일어난다.
- ✅ Memory Access와 Write-back은 load/store와 결과 반영을 담당하며, 캐시 미스와 쓰기 비용은 성능에 큰 영향을 준다.
- ✅ 인터럽트, 예외, 시스템 콜, 컨텍스트 스위치는 서로 다른 개념이며 백엔드 지연 분석에서 반드시 구분해야 한다.
- ✅ 현대 CPU는 파이프라이닝과 분기 예측과 out-of-order 실행으로 이 기본 흐름을 겹쳐서 더 빠르게 수행한다.