서로 다른 두 종류의 병목
Hazard는 한 코어 안에서 여러 명령어를 겹쳐 실행할 때 다음 단계로 진행하지 못하는 문제입니다. 반면 DMA와 Interrupt는 CPU와 입출력 장치가 일을 나누는 방법입니다. 발생 위치는 다르지만 목표는 같습니다. CPU가 불필요하게 기다리는 시간을 줄이는 것입니다.
CPU 내부: 명령어 A · B · C를 겹쳐 실행
└─ 의존성이나 자원 충돌 → Hazard
CPU 외부: CPU가 전송 설정 → DMA가 데이터 이동
└─ 완료 → Interrupt
먼저 파이프라인 안에서 무엇이 흐름을 막는지 살펴보고, 이후 장치 I/O가 CPU와 어떻게 비동기로 협력하는지 연결해 보겠습니다.
파이프라인 Hazard
설명용 5단계 파이프라인은 IF(인출), ID(해석), EX(실행), MEM(메모리 접근), WB(결과 기록)로 나눌 수 있습니다. 파이프라이닝은 명령어 하나의 지연 시간을 줄이기보다 여러 명령어의 단계를 겹쳐 처리량을 높입니다.
Cycle 1: A IF
Cycle 2: A ID | B IF
Cycle 3: A EX | B ID | C IF
Cycle 4: A MEM | B EX | C ID
Cycle 5: A WB | B MEM | C EX
그런데 다음 명령어가 필요한 데이터·실행 경로·하드웨어 자원을 확보하지 못하면 Hazard가 발생합니다. CPU는 진행을 잠시 멈추는 stall을 만들 수 있고, 그 결과 파이프라인에 유효한 작업이 없는 bubble이 생깁니다.
Data Hazard: 필요한 값이 아직 준비되지 않았다
ADD R1, R2, R3 ; R1 = R2 + R3
SUB R4, R1, R5 ; R1이 필요하다
두 번째 명령어는 첫 번째 명령어가 만든 R1을 읽어야 합니다. 이런 RAW(Read After Write)는 실제 데이터 흐름 때문에 생기는 진짜 의존성입니다. 결과가 레지스터 파일에 기록될 때까지 기다리지 않고 EX나 MEM 단계의 결과를 다음 실행 유닛 입력으로 보내는 forwarding으로 대기 시간을 줄일 수 있습니다.
ADD의 ALU 결과 ─────────→ SUB의 ALU 입력
forwarding
다만 forwarding이 모든 대기를 없애지는 못합니다. 예를 들어 load 직후 그 값을 사용하는 명령어는 데이터가 늦게 준비되어 한 사이클 이상 멈출 수 있습니다. 정확한 비용은 파이프라인 구조에 따라 달라집니다.
WAR(Write After Read)와 WAW(Write After Write)는 레지스터 이름을 재사용해 생기는 거짓 의존성입니다. 단순한 순차 파이프라인에서는 명령어가 정해진 순서로 읽고 쓰므로 보통 나타나지 않지만, 비순차 실행이나 쓰기 단계가 여러 개인 구조에서는 순서가 뒤바뀔 수 있습니다. 현대 CPU는 논리 레지스터를 서로 다른 물리 레지스터에 대응시키는 레지스터 리네이밍으로 이를 제거합니다.
Control Hazard: 다음 명령어 주소를 모른다
조건 분기의 결과가 확정되기 전에는 어느 경로에서 명령어를 가져와야 할지 모릅니다. 결과를 기다리면 프런트엔드가 쉬게 되므로 CPU는 분기 예측을 사용해 한 경로를 먼저 실행합니다.
분기 예측 성공 → 준비한 명령어를 계속 실행
분기 예측 실패 → 잘못된 경로를 flush → 올바른 주소에서 다시 fetch
예측 실패 비용은 잘못 들어온 작업을 버리고 파이프라인을 다시 채우는 시간입니다. 파이프라인 깊이와 CPU 구조에 따라 비용이 다르므로, 자세한 원리는 분기 예측 글에서 따로 설명했습니다.
Structural Hazard: 같은 자원을 동시에 요구한다
두 작업이 동시에 같은 하드웨어 자원을 요구하지만 수용량이 부족하면 구조적 Hazard가 발생합니다. 명령어 인출과 데이터 접근이 하나의 메모리 포트를 두고 경쟁하거나, 같은 종류의 실행 유닛으로 요청이 몰리는 상황이 예입니다.
명령어 캐시와 데이터 캐시를 분리하고, 실행 유닛·load/store 포트·메모리 포트를 늘리면 충돌을 줄일 수 있습니다. 하지만 면적과 전력 비용이 있으므로 현실의 CPU는 자원을 무한히 복제하지 않고 스케줄링과 대기를 함께 사용합니다.
Interrupt와 Exception
Interrupt는 타이머 만료, 키 입력, 네트워크 패킷 도착, DMA 완료처럼 현재 실행 중인 명령어 흐름과 독립적으로 발생할 수 있는 비동기 사건입니다. CPU가 장치 상태를 계속 확인하는 polling과 달리, 사건이 생겼을 때 장치가 처리를 요청합니다.
- 장치가 인터럽트 요청을 보냅니다.
- CPU는 아키텍처가 정한 안전한 경계에서 요청을 받아들입니다.
- 복귀에 필요한 PC와 상태를 하드웨어와 운영체제가 나누어 저장합니다.
- 인터럽트 벡터를 통해 ISR(Interrupt Service Routine)로 이동합니다.
- 필요한 처리를 마치고 저장한 실행 상태로 복귀합니다.
“현재 명령어를 항상 끝낸 뒤 인터럽트를 처리한다”는 설명은 직관적인 모델입니다. 실제로 어느 상태를 저장하고 어느 지점으로 복귀하는지는 ISA와 예외 종류에 따라 다르며, 프로그래머에게 일관된 상태를 보이도록 precise interrupt/exception을 제공하는지가 중요합니다.
Exception은 명령어 실행과 동기적으로 발생한다
0으로 나누기, 잘못된 명령어, 페이지 폴트처럼 현재 명령어 실행 때문에 생기는 사건은 보통 Exception이라고 구분합니다. x86 문맥에서는 복귀 위치와 복구 가능성에 따라 다음 용어를 사용합니다.
- Fault: 원인을 처리한 뒤 문제가 된 명령어를 다시 실행할 수 있습니다. 페이지 폴트가 대표적입니다.
- Trap: 명령어 실행 뒤 보고되어 다음 명령어로 복귀할 수 있습니다. 디버깅과 시스템 호출에서 볼 수 있습니다.
- Abort: 정확한 재시작 위치를 보장하기 어려운 심각한 오류입니다.
이 분류는 모든 ISA가 같은 이름과 의미로 사용하지는 않습니다. 예를 들어 어떤 아키텍처는 인터럽트와 예외를 더 큰 범주의 trap으로 묶습니다.
Interrupt는 Context Switch와 다르다
인터럽트를 처리하기 위해 실행 문맥 일부를 저장해도, 반드시 다른 프로세스나 스레드로 바뀌는 것은 아닙니다. ISR을 마친 뒤 원래 코드로 돌아갈 수 있습니다. 운영체제가 타이머 인터럽트를 계기로 스케줄러를 실행하고 다른 스레드를 선택했을 때 비로소 스케줄링 문맥의 context switch가 일어납니다.
Timer Interrupt
→ ISR과 Scheduler 실행
→ 같은 Thread 계속 실행 또는 다른 Thread로 Context Switch
DMA: CPU가 모든 바이트를 옮기지 않게 한다
DMA(Direct Memory Access)는 장치 또는 DMA 엔진이 CPU의 개별 load/store 명령을 거치지 않고 메모리와 데이터를 전송하는 방식입니다. “CPU를 거치지 않는다”는 말은 CPU가 완전히 관여하지 않는다는 뜻이 아닙니다. CPU와 드라이버가 버퍼를 준비하고 전송 방향·크기·장치가 사용할 DMA 주소를 설정하며, 실제 데이터 이동을 하드웨어가 담당한다는 뜻입니다.
CPU 복사: 장치 → CPU load/store 반복 → RAM
DMA 전송: CPU가 descriptor와 buffer 설정
장치/DMA 엔진 ─────────────→ RAM
CPU는 다른 작업 수행
- 운영체제와 드라이버가 DMA에 사용할 메모리를 할당하거나 매핑합니다.
- CPU가 장치에 DMA 주소, 크기와 전송 방향 등의 descriptor를 전달합니다.
- 장치 또는 DMA 엔진이 버스를 통해 메모리를 읽거나 씁니다.
- 완료되면 상태를 기록하고 보통 인터럽트로 CPU에 알립니다.
- 드라이버가 완료를 확인하고 대기 중인 작업을 깨우거나 다음 전송을 제출합니다.
DMA도 메모리 버스와 캐시 인터커넥트 같은 공유 자원을 사용하므로 “공짜 복사”는 아닙니다. CPU 연산을 다른 일에 쓸 수 있게 하지만 전송 설정, 주소 변환, 동기화, 버스 대역폭 비용은 남습니다. 작은 복사는 DMA 준비 비용이 더 클 수 있어 전송 크기와 플랫폼에 따라 선택해야 합니다.
DMA 주소는 CPU의 포인터와 같지 않을 수 있다
CPU 가상 주소, 물리 주소, 장치가 버스에서 사용하는 DMA 주소는 서로 다를 수 있습니다. 운영체제의 DMA API는 장치가 접근 가능한 주소를 만들고, 필요하면 IOMMU가 장치 주소를 물리 메모리로 변환합니다. 따라서 드라이버는 일반 포인터를 임의로 장치에 넘기지 않고 플랫폼의 매핑 API를 사용해야 합니다.
DMA와 메모리 일관성
DMA 장치와 CPU가 같은 버퍼를 사용하면 두 종류의 문제가 생깁니다. 첫째는 CPU 캐시와 메모리에 서로 다른 값이 남는 cache coherence, 둘째는 데이터와 “준비 완료” 플래그가 장치에 보이는 순서가 뒤바뀌는 memory ordering입니다.
CPU Cache: old data
RAM: DMA가 기록한 new data
CPU가 캐시를 그대로 읽으면 old data를 볼 수 있다.
일관성을 자동으로 유지하는 하드웨어도 있지만 모든 시스템이 그렇지는 않습니다. Linux DMA API는 용도에 따라 장기간 함께 사용하는 coherent mapping과 전송 단위로 소유권을 넘기는 streaming mapping을 구분합니다. 후자는 플랫폼에 따라 map/unmap 또는 sync 과정에서 캐시 정리와 무효화가 필요합니다.
coherent mapping도 메모리 접근 순서까지 자동 보장하지는 않습니다. 예를 들어 descriptor 내용을 먼저 쓰고 마지막에 valid 플래그를 세워야 한다면, 장치가 그 순서로 관찰하도록 적절한 메모리 배리어가 필요합니다. 캐시 일관성, 작업 완료 동기화와 접근 순서는 서로 다른 문제입니다.
게임 리소스 스트리밍으로 연결하기
오픈 월드에서 다음 지역의 텍스처를 준비하는 상황을 생각해 보겠습니다. 실제 경로는 저장 장치, 운영체제, 그래픽 API와 하드웨어에 따라 달라지지만 개념적인 흐름은 다음과 같습니다.
1. 게임이 비동기 파일 읽기를 요청
2. OS·드라이버가 저장 장치의 DMA 전송을 준비
3. SSD → 시스템 메모리로 데이터 이동
4. 완료 Interrupt → OS가 요청 완료 처리
5. CPU가 압축 해제·형식 변환 작업을 스케줄
6. 그래픽 API에 copy command와 동기화 조건 기록
7. GPU copy engine이 시스템 메모리 → VRAM 전송
8. fence/semaphore 완료 뒤 렌더링에서 리소스 사용
여기서 DMA와 Interrupt는 CPU를 데이터 복사와 상태 확인 반복에서 해방합니다. 그렇다고 CPU 비용이 사라지는 것은 아닙니다. 요청 제출, 압축 해제, 드라이버 처리와 동기화가 남고, 저장 장치·PCIe·메모리·GPU 대역폭 사이에 병목이 생길 수 있습니다.
Hazard는 이 흐름의 다른 층에서 발생합니다. CPU가 완료 처리와 게임 로직을 실행하는 동안에도 각 코어 내부에서는 데이터 의존성, 분기와 실행 포트 경쟁이 처리량을 제한합니다. 즉, DMA가 시스템 수준의 대기를 숨기고 파이프라인 최적화가 코어 내부의 대기를 줄입니다.
정리
| 개념 | 발생 위치 | 핵심 역할 또는 문제 |
| Hazard | CPU 파이프라인 내부 | 의존성, 분기, 자원 충돌이 다음 단계 진행을 막습니다. |
| Interrupt | CPU와 장치·시스템의 경계 | 비동기 사건을 알려 CPU가 처리 루틴으로 이동하게 합니다. |
| Exception | 현재 명령어 실행 | 명령어 때문에 동기적으로 발생한 사건을 처리합니다. |
| DMA | 장치와 메모리 사이 | CPU의 바이트별 복사 없이 하드웨어가 데이터를 전송합니다. |
- RAW는 실제 데이터 의존성이고, WAR·WAW는 주로 비순차 실행에서 레지스터 이름 재사용 때문에 생깁니다.
- forwarding, stall, 분기 예측, 레지스터 리네이밍과 자원 복제가 각 Hazard를 줄입니다.
- DMA는 CPU가 설정하고 장치가 전송하며, 완료는 흔히 Interrupt로 통보합니다.
- DMA 버퍼는 주소 매핑뿐 아니라 캐시 일관성, 메모리 순서와 작업 완료 동기화도 필요합니다.
한 문장으로 압축하면, Hazard는 코어 안의 겹친 실행을 막는 문제이고, DMA는 데이터 이동을 하드웨어에 위임하는 방식이며, Interrupt는 비동기 사건과 전송 완료를 CPU에 알리는 메커니즘입니다.
참고 자료
'컴퓨터 구조' 카테고리의 다른 글
| [컴퓨터 구조] CPU 구조와 명령어 실행 흐름 (1) | 2026.07.20 |
|---|---|
| [TIL] 메모리 참조 지역성 (0) | 2026.07.13 |