핵심 인사이트 (3줄 요약)
- 본질: WAR (Write After Read)는 앞 명령어가 어떤 레지스터 값을 아직 읽지 않았는데, 뒤 명령어가 같은 이름의 레지스터에 먼저 써버려 앞 명령어가 원래 읽어야 할 옛값을 잃는 반의존성 (Anti-dependency) 문제다.
- 가치: 단순 순차 파이프라인에서는 잘 드러나지 않지만, 비순차 실행 (Out-of-Order Execution, OoO)과 다중 실행 유닛 환경에서는 병렬성을 크게 제한할 수 있는 대표적인 이름 의존성 (Name Dependency)이다.
- 판단 포인트: WAR는 실제 데이터 전달이 아니라 "이름 충돌" 때문에 생기므로, 레지스터 리네이밍 (Register Renaming)으로 논리 레지스터와 물리 레지스터를 분리하면 구조적으로 제거할 수 있다.
Ⅰ. 개요 및 필요성
WAR는 먼저 등장한 명령어가 레지스터를 읽기 전에, 나중 명령어가 같은 레지스터에 새 값을 기록해 읽기 순서를 깨뜨리는 해저드다. 표면적으로는 "뒤 명령어가 빨랐다"는 문제처럼 보이지만, 본질은 계산 관계가 아니라 같은 이름표를 공유하는 자원 관리 문제에 가깝다. 즉 앞 명령어와 뒤 명령어가 실제로 값을 주고받는 것은 아니어도, 같은 논리 레지스터 이름을 쓰기 때문에 충돌이 발생한다.
고전적 5단 파이프라인에서는 보통 읽기가 ID (Instruction Decode) 단계에서 먼저 일어나고, 쓰기가 WB (Write Back) 단계에서 나중에 일어나므로 WAR가 자연스럽게 숨겨진다. 그러나 현대 CPU (Central Processing Unit)는 준비된 연산부터 먼저 실행하고, 긴 지연 명령어는 뒤로 밀리며, 결과는 가능한 빨리 기록하려 한다. 이때 느린 앞 명령어의 읽기보다 빠른 뒤 명령어의 쓰기가 앞서면, 프로그램 순서가 보장해야 할 관찰 가능한 값이 훼손된다.
결국 WAR를 이해해야 하는 이유는 단순히 해저드 분류를 외우기 위해서가 아니다. 고성능 마이크로아키텍처가 왜 레지스터 이름을 그대로 믿지 않고 별도의 매핑 계층을 두는지를 설명하는 핵심 근거이기 때문이다. WAR를 해결하지 못하면 독립적인 명령어도 이름만 겹친다는 이유로 함께 묶여 병렬 실행 기회를 잃는다.
┌────────────────────────────────────────────────────────────────────────────┐
│ WAR의 직관: 읽어야 할 옛값이 먼저 덮어써지는 문제 │
├────────────────────────────────────────────────────────────────────────────┤
│ 프로그램 순서 │
│ I1: R2 ← R1 + R3 (R1을 읽어야 함) │
│ I2: R1 ← R4 - R5 (R1에 새 값을 씀) │
│ │
│ 올바른 의미 │
│ I1은 "옛 R1"을 읽은 뒤 계산해야 함 │
│ │
│ 잘못된 실행 │
│ I2가 먼저 R1을 갱신 ───────────────▶ I1이 "새 R1"을 읽음 │
│ │
│ 결과 │
│ 데이터 흐름은 없는데 이름이 겹쳐 정답이 바뀜 │
└────────────────────────────────────────────────────────────────────────────┘
이 그림이 보여주는 핵심은 WAR가 "뒤 명령어 결과를 앞 명령어가 받아 쓰는" 상황이 아니라, 앞 명령어가 봐야 할 과거 상태를 뒤 명령어가 먼저 지워버리는 상황이라는 점이다. 그래서 WAR는 진성 의존성보다 더 교묘하지만, 동시에 더 구조적으로 해결 가능한 문제다.
- 📢 섹션 요약 비유: 앞사람이 사전에서 단어를 찾고 있는데, 뒷사람이 같은 페이지를 새 내용으로 덮어붙여 버리는 상황과 같다. 서로 대화한 것은 없지만, 같은 책장을 써서 충돌이 난다.
Ⅱ. 아키텍처 및 핵심 원리
WAR는 프로그램 순서와 실제 실행 순서가 분리될 때 뚜렷해진다. 프로그램 순서에서는 I1이 먼저 레지스터를 읽고 I2가 나중에 써야 하지만, 하드웨어는 기능 유닛 가용성, 메모리 지연, 예약역 상태에 따라 I2를 먼저 실행 완료시킬 수 있다. 이때 중요한 것은 I2의 쓰기가 빨랐다는 사실 자체가 아니라, I1의 읽기가 아직 확정되지 않았다는 점이다.
WAR가 생기는 조건
| 조건 | 설명 | WAR 발생 여부 |
|---|---|---|
| 앞 명령어가 어떤 레지스터를 읽음 | 예: I1: ADD R6, R1, R7 | 필요 조건 |
| 뒤 명령어가 같은 레지스터에 씀 | 예: I2: MUL R1, R8, R9 | 필요 조건 |
| 읽기보다 쓰기가 먼저 관찰됨 | OoO, 가변 지연, 다중 실행 유닛 | 실제 충돌 |
| 읽기와 쓰기가 순서대로 고정됨 | 단순 in-order 5단 파이프라인 | 보통 숨겨짐 |
아래 시간도는 왜 WAR가 단순 순차 파이프라인보다 비순차 구조에서 문제 되는지 보여준다.
┌────────────────────────────────────────────────────────────────────────────┐
│ 프로그램 순서와 실제 완료 순서가 뒤집힐 때의 WAR │
├────────┬────────────┬────────────┬────────────┬────────────┬────────────┤
│ Cycle │ 1 │ 2 │ 3 │ 4 │ 5 │
├────────┼────────────┼────────────┼────────────┼────────────┼────────────┤
│ I1 │ Issue │ wait │ wait/read? │ EX │ Commit │
│ │ (R1 필요) │ │ │ │ │
│ I2 │ Issue │ EX │ Write R1 │ done │ Commit │
├────────┴────────────┴────────────┴────────────┴────────────┴────────────┤
│ 위험: I1이 아직 R1을 읽지 않았는데 Cycle 3에서 I2가 R1을 먼저 갱신함 │
└────────────────────────────────────────────────────────────────────────────┘
이 상황은 메모리 의존성과 달리 값의 생산-소비 관계가 없다. I2가 만든 새 R1은 I1에 필요하지 않다. I1은 오히려 과거의 R1이 필요하다. 그래서 WAR는 데이터플로우 상 필연적인 기다림이 아니라, 아키텍처가 논리 레지스터라는 좁은 이름 공간을 재사용하면서 생긴 인공적 병목이다.
이를 해결하는 대표 기법이 레지스터 리네이밍이다. 하드웨어는 논리 레지스터 R1을 그대로 저장 위치로 쓰지 않고, 시점마다 다른 물리 레지스터에 매핑한다. 그러면 I1은 이전 매핑된 물리 레지스터에서 옛값을 읽고, I2는 새 물리 레지스터에 결과를 써도 되므로 서로 간섭하지 않는다.
┌────────────────────────────────────────────────────────────────────────────┐
│ 레지스터 리네이밍이 WAR를 끊는 방식 │
├────────────────────────────────────────────────────────────────────────────┤
│ 논리 이름: R1 │
│ 이전 매핑: R1 ───────────────▶ P12 (옛값 저장) │
│ 새 명령 I2 배정: R1 ───────────────▶ P27 (새값 저장 예정) │
│ │
│ I1은 P12를 읽음 ───────────────▶ 옛값 유지 │
│ I2는 P27에 씀 ───────────────▶ 새값 생성 │
│ │
│ 효과: 논리 이름은 같아도 실제 저장 위치가 달라 WAR가 사라짐 │
└────────────────────────────────────────────────────────────────────────────┘
핵심은 "값을 늦게 쓰게 만드는 것"보다 "값을 다른 곳에 쓰게 만드는 것"이다. WAR는 시간만 조정해서 버틸 수는 있지만, 리네이밍은 충돌의 원인 자체를 제거한다는 점에서 더 근본적이다.
- 📢 섹션 요약 비유: 같은 사물함 번호를 두 학생이 번갈아 쓰면 먼저 꺼내야 할 교과서가 사라질 수 있다. 하지만 학교가 임시 사물함을 더 배정해 주면 번호표는 같아 보여도 실제 보관 장소가 달라져 싸움이 없어진다.
Ⅲ. 비교 및 연결
WAR를 제대로 이해하려면 RAW (Read After Write), WAW (Write After Write)와의 경계를 분명히 봐야 한다. 세 해저드는 모두 레지스터 이름이 겹쳐 보이지만, 진짜로 데이터가 흐르는지, 단순히 이름이 충돌하는지에 따라 처리 방식이 완전히 달라진다.
| 항목 | RAW (Read After Write) | WAR (Write After Read) | WAW (Write After Write) |
|---|---|---|---|
| 본질 | 뒤 명령어가 앞 결과를 필요로 함 | 앞 명령어가 옛값을 읽기 전 덮어씀 | 두 명령어가 같은 목적지에 씀 |
| 의존성 성격 | 진성 의존성 (True Dependency) | 반의존성 (Anti-dependency) | 출력 의존성 (Output Dependency) |
| 리네이밍 효과 | 제거 불가 | 제거 가능 | 제거 가능 |
| 대표 대응 | 포워딩, 스톨, OoO 스케줄링 | 리네이밍, 읽기/쓰기 순서 보장 | 리네이밍, 커밋 순서 보장 |
| 설계 의미 | 실제 데이터 전달 경로 필요 | 이름 공간 확장 필요 | 최종 결과의 순서 보장 필요 |
이 비교에서 중요한 점은 WAR와 WAW가 모두 "가짜 의존성" 계열이라는 사실이다. 둘 다 저장 위치의 이름이 겹쳐서 생기며, 새 값을 위한 별도 물리 위치를 제공하면 충돌이 크게 줄어든다. 반면 RAW는 실제로 앞 결과가 뒤 명령어의 입력이므로, 이름만 바꾼다고 해결되지 않는다.
WAR는 데이터 해저드 전체 문맥에서도 위치가 명확하다. 단순 파이프라인에서는 RAW가 지배적이고, 파이프라인이 깊어지고 OoO가 도입될수록 WAR·WAW의 중요도가 커진다. 그래서 WAR는 파이프라인 해저드 단원 안에서 비순차 실행, 재주문 버퍼 (Reorder Buffer, ROB), 예약역 (Reservation Station), 토마술로 알고리즘 (Tomasulo's Algorithm)으로 이어지는 연결 고리 역할을 한다.
즉 WAR는 개별 정의를 외우는 항목이 아니라, "왜 현대 프로세서가 논리 상태와 물리 저장 위치를 분리하는가"를 보여주는 전환점이다. 단순한 레지스터 파일에서 끝나던 이야기가, 동적 스케줄링과 리네이밍 중심 마이크로아키텍처로 확장되는 문턱이 바로 여기에 있다.
- 📢 섹션 요약 비유: RAW는 앞 사람이 재료를 만들어 줘야 뒤 사람이 요리할 수 있는 관계이고, WAR는 같은 도마 이름표 때문에 서로 엉키는 관계다. 전자는 기다림이 본질이고, 후자는 도마를 하나 더 주면 풀린다.
Ⅳ. 실무 적용 및 기술사 판단
실무 관점에서 WAR 대응은 단순히 "해저드를 막는다"가 아니라, 얼마나 공격적으로 병렬성을 끌어낼지 결정하는 문제다. 고성능 코어는 여러 실행 유닛과 넓은 이슈 폭을 사용하므로, 이름 충돌을 그대로 두면 발행 가능한 명령어 수가 급격히 줄어든다. 따라서 충분한 물리 레지스터 파일 (Physical Register File, PRF), 빠른 리네임 테이블, 정확한 커밋 제어가 함께 설계되어야 한다.
반대로 저전력 in-order 코어에서는 WAR가 구조적으로 거의 드러나지 않기 때문에, 복잡한 리네이밍 로직을 생략해 면적과 전력을 줄이는 선택이 가능하다. 즉 WAR의 중요도는 "해저드 자체의 위험성"만이 아니라, 어떤 실행 모델을 채택했는가에 의해 결정된다. 이것이 기술사 답안에서 아키텍처 문맥을 함께 써야 하는 이유다.
설계 판단 체크리스트
- 실행 유닛이 여러 개이고 OoO 폭이 넓다면, 논리 레지스터 수보다 충분히 큰 PRF를 확보했는가?
- 리네이밍은 되었더라도, 읽기 포트·쓰기 포트 병목 때문에 새 병목이 생기지 않는가?
- 예외·인터럽트 발생 시 재주문 버퍼를 통해 프로그램 순서를 복원할 수 있는가?
- 전력 예산이 작은 코어라면 WAR 제거를 위한 복잡한 동적 기법보다 순차 실행 단순화가 더 합리적이지 않은가?
안티패턴
- WAR를 RAW와 같은 방식으로만 이해해 불필요한 스톨을 과도하게 거는 설계
- 물리 레지스터 수를 너무 아끼다가 리네임 자원이 고갈되어 성능이 급락하는 설계
- 리네이밍은 넣었지만 커밋·복구 경로가 약해 예외 처리 시 상태 일관성을 잃는 설계
실제 설계 면접이나 기술사 서술에서는 "WAR는 리네이밍으로 제거 가능"에서 멈추면 부족하다. 왜 순차 파이프라인에서는 덜 보이는지, 왜 OoO에서 중요해지는지, 그 대가로 어떤 면적·전력 비용을 치르는지까지 언급해야 판단형 답이 된다.
- 📢 섹션 요약 비유: 큰 공항은 같은 탑승구 번호만 믿지 않고, 실제 비행기를 여러 주기장에 분산 배치해 운항을 돌린다. 작은 공항은 굳이 그렇게까지 하지 않아도 되므로, 공항 규모에 따라 운영 방식이 달라진다.
Ⅴ. 기대효과 및 결론
WAR를 제대로 처리하면 독립적인 명령어들이 이름 충돌 때문에 헛되이 묶이지 않게 된다. 그 결과 명령어 수준 병렬성 (Instruction-Level Parallelism, ILP)을 더 많이 끌어낼 수 있고, 실행 유닛 가동률이 올라가며, 긴 지연 명령어가 있어도 다른 연산을 계속 흘려보낼 수 있다. 즉 WAR 제거는 단순한 정확성 보장 이상의 의미를 가지며, 고성능 프로세서의 처리량 기반을 지탱한다.
물론 대가도 있다. 리네이밍 로직, PRF, ROB, 복구 메커니즘은 모두 전력과 면적을 소모하고 검증을 어렵게 만든다. 그래서 모든 CPU가 같은 수준의 WAR 대응 구조를 택하지는 않으며, 모바일 효율 코어와 서버용 슈퍼스칼라 코어의 선택은 달라질 수밖에 없다.
결국 WAR는 "뒤 명령어가 너무 빨라서 생긴 사고"가 아니라, 논리 이름 공간이 좁을 때 병렬성이 받는 제약으로 기억하는 것이 정확하다. 그리고 현대 CPU의 해법은 기다림을 강요하는 것이 아니라, 이름과 실제 저장 위치를 분리해 병렬성의 여지를 다시 열어 주는 것이다.
- 📢 섹션 요약 비유: 같은 이름의 서랍 하나만 쓰면 서로 순서를 양보해야 하지만, 보이지 않는 여분 서랍을 많이 두면 각자 자기 물건을 따로 넣고 꺼낼 수 있다. WAR 해결은 결국 서랍을 더 똑똑하게 배정하는 일이다.
📌 관련 개념 맵
| 개념 | 연결 포인트 |
|---|---|
| 데이터 해저드 (Data Hazard) | WAR는 파이프라인 데이터 해저드의 한 유형이지만, 진성 데이터 전달보다 이름 충돌 성격이 강하다. |
| 비순차 실행 (Out-of-Order Execution, OoO) | 프로그램 순서와 실제 실행 순서가 어긋날수록 WAR가 현실 문제로 드러난다. |
| 레지스터 리네이밍 (Register Renaming) | WAR를 구조적으로 제거하는 대표 해법으로, 논리 레지스터와 물리 레지스터를 분리한다. |
| 재주문 버퍼 (Reorder Buffer, ROB) | 비순차 실행 뒤에도 최종 상태를 프로그램 순서대로 커밋해 예외와 정합성을 관리한다. |
| 토마술로 알고리즘 (Tomasulo's Algorithm) | WAR·WAW 같은 이름 의존성을 태그 기반 동적 스케줄링과 리네이밍으로 완화하는 대표 구조다. |
📈 관련 키워드 및 발전 흐름도
순차 파이프라인의 단순 레지스터 사용
│
▼
데이터 해저드 구분: RAW / WAR / WAW
│
▼
비순차 실행 (Out-of-Order Execution, OoO) 도입
│
▼
WAR·WAW가 실제 병목으로 부상
│
▼
레지스터 리네이밍 (Register Renaming)
│
▼
재주문 버퍼 (ROB) · 예약역 · 토마술로 알고리즘
이 흐름은 단순한 파이프라인 교재 개념이, 현대 동적 스케줄링 프로세서의 핵심 설계 요소로 확장되는 과정을 보여준다.
👶 어린이를 위한 3줄 비유 설명
- 형이 오래된 메모를 읽고 있는데, 동생이 같은 메모장에 먼저 새 내용을 써 버리면 형은 원래 글을 못 읽어요.
- 이게 바로 WAR이고, 둘이 같은 이름의 메모장을 써서 생긴 충돌이에요.
- 똑똑한 컴퓨터는 겉으로는 같은 메모장처럼 보여도 실제로는 다른 메모장을 나눠 줘서 싸움을 없애요.