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

Pipeline: 명령어를 겹쳐 실행해 처리량을 높이는 하드웨어 기법

CPU 파이프라인은 하나의 명령어 처리 과정을 여러 단계로 나누고, 각 단계를 서로 다른 명령어가 동시에 사용하도록 겹쳐 실행하는 구조다. 단일 명령어의 지연 시간(latency)은 줄지 않지만 단위 시간당 처리하는 명령어 수(throughput)가 늘어난다. 데이터 해저드, 제어 해저드, 구조적 해저드가 발생하면 파이프라인이 멈추거나(stall) 비워지므

송민성6분 읽기

1. 개념

파이프라인(Pipeline)은 하나의 작업을 여러 개의 독립된 단계(stage)로 나누고, 각 단계를 전담하는 하드웨어 유닛에 서로 다른 작업을 동시에 흘려보내는 실행 방식이다. 공장의 조립 라인과 같은 구조로, 한 제품이 완성되기까지 걸리는 시간(latency)은 줄지 않지만 단위 시간당 완성되는 제품 수(throughput)는 늘어난다.

CPU에서는 이를 명령어 파이프라인(Instruction Pipeline)이라고 부르며, 대표적으로 MIPS 아키텍처 기준 5단계 구조를 쓴다.

  • IF (Instruction Fetch): 메모리에서 명령어를 읽어온다.
  • ID (Instruction Decode): 명령어를 해석하고 레지스터 값을 읽는다.
  • EX (Execute): ALU 연산이나 주소 계산을 수행한다.
  • MEM (Memory Access): 메모리에 데이터를 읽거나 쓴다.
  • WB (Write Back): 결과를 레지스터에 기록한다.

2. 왜 사용하는가

파이프라인이 없다면 한 명령어가 IF→ID→EX→MEM→WB를 모두 마친 뒤에야 다음 명령어를 시작할 수 있다. 각 단계가 1클럭이라고 하면 명령어 하나에 5클럭이 걸리고, N개의 명령어를 처리하는 데 5N클럭이 필요하다.

파이프라인을 적용하면 매 클럭마다 새 명령어를 IF 단계로 밀어 넣을 수 있으므로, 이상적인 경우 첫 명령어가 끝나는 데 5클럭이 걸린 뒤부터는 매 클럭마다 명령어 하나씩 완료된다. N개의 명령어를 처리하는 데 걸리는 클럭 수는 대략 5 + (N-1)로, N이 커질수록 클럭당 명령어 수(IPC, Instructions Per Cycle)가 1에 가까워진다. 이는 트랜지스터를 추가로 늘리지 않고도 하드웨어 유닛의 유휴 시간을 줄여 처리량을 끌어올리는 방식이다.

3. 동작 원리

파이프라인은 각 단계마다 파이프라인 레지스터(pipeline register)를 두어 이전 단계의 결과를 다음 단계로 전달한다. 클럭이 한 번 뛸 때마다 모든 명령어가 한 단계씩 동시에 전진한다.

문제는 명령어들이 서로 독립적이지 않을 때 발생하며, 이를 해저드(Hazard)라고 부른다.

  • 구조적 해저드(Structural Hazard): 두 단계가 동시에 같은 하드웨어 자원(예: 단일 메모리 포트)을 요구할 때 발생한다. 명령어 메모리와 데이터 메모리를 분리하는 방식(하버드 구조)으로 완화한다.
  • 데이터 해저드(Data Hazard): 뒤 명령어가 앞 명령어의 결과를 아직 WB 단계에서 쓰기 전에 필요로 할 때 발생한다. 포워딩(forwarding, 결과를 EX 단계에서 바로 다음 명령어의 EX 단계로 우회 전달)이나 스톨(stall, 파이프라인을 잠시 멈춤)로 해결한다.
  • 제어 해저드(Control Hazard): 분기 명령어의 결과가 확정되기 전까지 다음에 가져올 명령어를 알 수 없을 때 발생한다. 분기 예측(branch prediction)을 사용하고, 예측이 틀리면 이미 파이프라인에 들어온 명령어들을 무효화하는 플러시(flush)가 일어난다.

4. 코드 예제

5단계 파이프라인이 클럭마다 어떻게 겹쳐 실행되는지, 그리고 데이터 해저드로 인한 스톨이 발생했을 때 사이클이 어떻게 늘어나는지 시뮬레이션한다.

python
from dataclasses import dataclass, field STAGES = ["IF", "ID", "EX", "MEM", "WB"] @dataclass class Instruction: name: str depends_on: str | None = None # 데이터 의존성이 있는 명령어 이름 stage_at_cycle: dict[int, str] = field(default_factory=dict) def simulate_pipeline(instructions: list[Instruction]) -> None: """ 데이터 해저드가 있으면 ID 단계에서 1클럭 스톨을 발생시키는 단순화된 5단계 파이프라인 시뮬레이터. """ cycle = 1 completed_stage: dict[str, dict[str, int]] = {} # {명령어: {단계: 완료된 클럭}} pc = 0 # 다음에 IF를 시작할 명령어 인덱스 in_flight: list[tuple[Instruction, int]] = [] # (명령어, 단계 인덱스) while pc < len(instructions) or in_flight: # 뒤에서부터 처리해야 같은 클럭 내 순서 꼬임을 방지한다 next_in_flight = [] stalled_this_cycle = False for instr, stage_idx in reversed(in_flight): stage_name = STAGES[stage_idx] # 데이터 해저드 체크: ID 단계 진입 시 의존 대상이 아직 WB 전이면 스톨 if stage_name == "ID" and instr.depends_on: dep_wb_cycle = completed_stage.get(instr.depends_on, {}).get("WB") if dep_wb_cycle is None or dep_wb_cycle >= cycle: stalled_this_cycle = True next_in_flight.append((instr, stage_idx)) # 제자리 유지 continue completed_stage.setdefault(instr.name, {})[stage_name] = cycle instr.stage_at_cycle[cycle] = stage_name if stage_idx + 1 < len(STAGES): next_in_flight.append((instr, stage_idx + 1)) in_flight = list(reversed(next_in_flight)) # 스톨이 없을 때만 새 명령어를 IF로 투입 if not stalled_this_cycle and pc < len(instructions): new_instr = instructions[pc] in_flight.insert(0, (new_instr, 0)) completed_stage.setdefault(new_instr.name, {})["IF"] = cycle new_instr.stage_at_cycle[cycle] = "IF" pc += 1 cycle += 1 # 사이클 다이어그램 출력 total_cycles = cycle - 1 print(f"{'명령어':<8}" + "".join(f"{c:>4}" for c in range(1, total_cycles + 1))) for instr in instructions: row = [instr.stage_at_cycle.get(c, "") for c in range(1, total_cycles + 1)] print(f"{instr.name:<8}" + "".join(f"{s:>4}" for s in row)) if __name__ == "__main__": instrs = [ Instruction("I1"), Instruction("I2", depends_on="I1"), # I1 결과에 의존 -> 스톨 발생 Instruction("I3"), ] simulate_pipeline(instrs)

실행하면 I2가 I1의 결과를 기다리며 ID 단계에서 멈추고, 그로 인해 I3의 IF 시작도 함께 늦춰지는 것을 사이클 다이어그램으로 확인할 수 있다.

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

  • 파이프라인 깊이(depth)를 k, 명령어 개수를 N이라 하면 이상적인 총 소요 클럭은 k + (N-1)이다. 파이프라인이 없을 때는 k × N클럭이 필요하므로, N이 충분히 크면 이론상 최대 speedup은 k배에 근접한다.
  • 실제로는 해저드로 인한 스톨과 분기 예측 실패로 인한 플러시 때문에 IPC가 1보다 낮아진다. 분기 예측 실패 시 손실되는 클럭 수(branch misprediction penalty)는 파이프라인 깊이에 비례하며, 파이프라인이 깊을수록(예: 20단계 이상의 슈퍼스칼라 프로세서) 예측 실패 페널티도 커진다.
  • 정확한 페널티 클럭 수나 IPC 수치는 마이크로아키텍처와 워크로드에 따라 달라지므로, 특정 프로세서의 수치는 해당 제조사의 최적화 매뉴얼을 확인해야 한다.

6. 실무 사용 사례

  • 컴파일러의 명령어 스케줄링(instruction scheduling)은 데이터 의존성이 있는 명령어 사이에 독립적인 명령어를 재배치해 스톨을 줄인다.
  • 분기가 많은 코드에서 분기 예측 친화적인 작성(예: 반복되는 조건을 앞쪽에 배치, 예측 가능한 패턴 유지)은 파이프라인 플러시 빈도를 낮춘다.
  • 소프트웨어 파이프라이닝(software pipelining)은 반복문의 여러 이터레이션을 겹쳐 스케줄링해 하드웨어 파이프라인 활용률을 높이는 컴파일러 최적화 기법이다.
  • CI/CD의 "빌드 파이프라인" 같은 용어는 이 개념에서 이름을 빌려온 것으로, 여러 단계를 겹쳐 처리한다는 아이디어는 같지만 하드웨어 명령어 파이프라인과 구현 방식은 다르다.

7. 주의할 점

  • 파이프라인 깊이를 늘리면 클럭 속도를 높이기 쉬워지지만, 분기 예측 실패나 캐시 미스 같은 이벤트의 페널티도 함께 커진다. 이를 무작정 늘리는 것이 항상 이득은 아니다.
  • 데이터 해저드를 포워딩만으로 모두 해결할 수는 없다. 로드 명령어 뒤에 바로 그 값을 쓰는 명령어가 오면(load-use hazard) 포워딩으로도 한 클럭 스톨을 피할 수 없는 경우가 있다.
  • 인터럽트나 예외가 발생하면 파이프라인에 있던 명령어들의 상태를 정확히 되돌려야 하는 정밀 예외(precise exception) 처리가 필요하며, 이는 파이프라인 설계를 복잡하게 만드는 요인이다.

8. 핵심 정리

파이프라인은 하나의 작업을 여러 단계로 쪼개 겹쳐 실행함으로써 개별 작업의 지연 시간이 아니라 전체 처리량을 높이는 기법이다. CPU에서는 IF-ID-EX-MEM-WB 같은 단계로 구현되며, 구조적·데이터·제어 해저드가 이상적인 성능을 갉아먹는 주요 원인이다. 포워딩, 분기 예측, 컴파일러의 명령어 재배치는 모두 이 해저드를 줄이기 위한 장치이며, 파이프라인 깊이와 예측 실패 페널티 사이의 트레이드오프를 이해하는 것이 컴퓨터 구조를 다루는 개발자에게 필요한 기초 체력이다.

© 2026 Tyler Song