파이프라인 (CPU)

큰숲백과, 나무를 보지 말고 큰 숲을 보라.

CPU에서 파이프라인(Pipeline)이란 CPU가 명령어를 처리하는 작업을 여러 단계로 나누어 각 단계별 상태를 내부 플립플롭에 저장하고 매 클럭 사이클이 돌 때마다 클럭 엣지가 들어오는 시점에 다음 단계로 데이터들을 연산하여 그 결과를 넘겨주는 작업을 연속하는 구조를 말한다. RISC 연구 과정에서 적극적으로 도입되었기에 RISC ISA를 가지는 CPU의 대표 특징으로 자리 잡았으며, 회로 내 개별 단계의 로직이 파이프라인 구조가 없는 CPU의 처리 로직에 비해 간소화되어 더 높은 클럭을 넣을 수 있게 된다.

상세[편집 | 원본 편집]

파이프라인 구조가 있는 CPU의 작업은 크게 Fetch, Decode, Execute, Write Back 단계로 나뉜다. 물론 큰 흐름이 이렇다는 것이고 실제로는 GHz 수준의 클럭 진동수 달성을 위해 종속성 분석 및 실행 유닛 내 파이프라이닝 등의 기법을 써서 10단계 이상으로 넘어가는 경우가 많다.

  • Fetch는 메모리에서 특정한 코드를 가져오는 작업이다. 전통적인 디자인에서는 1개의 명령어가 한 클럭 사이클에 들어오나 현대에 들어와서는 CPU가 한번에 여러 명령어를 뽑는 슈퍼스칼라 기법이 적용되어 IPC(Instructions Per Cycle, 단위 사이클 당 명령어 수)를 늘릴 수 있다.
  • Decode는 가져온 코드에 따라 작동할 연산부를 결정하고 필요한 데이터를 연산부에 넘겨준다. 이 작업을 하는 CPU 내부 유닛을 디코더(Decoder)라고 한다.
  • Execute는 들어온 데이터를 산술 연산 유닛(Arithmetic Logic Unit)등의 연산부나 MMU, FPU 등이 가공하고 실행시킨다.
  • Write Back은 실행한 결과물에 따라 레지스터나 주기억장치에 연산의 결과물을 기억시키는 작업이다.

여기서 Fetch부터 Write Back까지 코드가 1개 들어가고 1개 나올 때까지 기다리는 것은 성능 향상에 도움이 되지 않으므로 컨베이어 벨트를 사용하는 공장처럼 각 단계별로 작업을 연속적으로 할 수 있도록 현재 작업 중인 코드를 기억하는 레지스터를 단계별로 배치하는데, 이를 파이프라이닝 기법이라고 한다. 파이프라이닝은 각 단계별로 1개~n개씩 배치되어 클럭 사이클(반복되는 클럭 변화)마다 1단계씩 다음 단계로 코드/데이터를 넘기므로 각 레지스터 사이에 배치된 트랜지스터 논리 소자 배치가 단순해져 트랜지스터를 지나갈 때마다 발생하는 전기 신호 지연을 줄일 수 있어 클럭 속도를 높일 수 있다.

한계점[편집 | 원본 편집]

그러나 파이프라인 구조가 장점만 있는 것이 아니다. CPU에 들어가는 클럭이 높아진다고 CPU에 들어간 명령어가 처리되어 나오기까지 시간이 모든 경우에 대해 줄어드는 건 아니기 때문에 파이프라인의 일부 단계만 거쳐도 되는 분기 명령어나 단순 연산은 처리 시간이 확실히 빨라지지만 파이프라인의 모든 단계를 거치면서 처리 시간까지 긴 명령어, 특히 메모리에 데이터를 읽고 쓰는 Load/Store 기능 등은 처리 시간 개선의 정도가 크지 않다(이쪽은 CPU와 상관 없는 메모리 계층 구조 개선에서 더 큰 성능 향상을 얻을 수 있기도 하다).

게다가 근본적으로 파이프라인의 모든 단계의 연산 결과물이 유효하다는 가정을 하기 때문에 만일 중간에 다음에 들어올 명령어가 무효화될 수 있는 상황에는 성능 향상 효과가 나타나지 않는다. 대표적으로 분기 명령어에 의한 jump와 예외 처리 인터럽트 발생 등이 있다. 이런 명령어를 처리해야 하는 경우 Fetch 단계에 해당 종류의 명령어가 들어간 이후 적어도 Decode, 보통 Execute 단계에 갈 때까지 Fetch를 할 수 없는 상황이 발생한다. 이 경우 NOP(No operation, 연산 기능을 하지 않음)를 대신 채워 넣는데 이를 파이프라인 버블이라고 한다. 파이프라인 버블은 파이프라인 위험(Pipeline Hazard, 보통 CPU 이야기를 이미 하고 있다면 Hazard로 간단하게 말한다)에서 Control Hazard로 분류한다.

전술한 파이프라인 위험은 Structural Hazard, Data Hazard, Control Hazard로 나뉜다. 바로 앞에서 설명한 Control Hazard를 제외한 다른 Hazard에 대해 설명하자면,

  • Structural Hazard: CPU가 메모리와 통신할 때에는 보통 한 번에 하나의 요청을 보내는데, 이때 명령어를 저장하는 메모리와 데이터를 저장하는 메모리가 같은 경우 Load/Store 명령어처럼 명령어 그 자체와 타겟 데이터를 동시에 메모리에서 가져오는 명령어를 한번에 처리하지 못하고 두 차례에 나눠서 처리하게 되어 발생하는 성능 저하 위험이다. 이건 폰 노이만 구조를 확장하여 데이터 보관 메모리와 명령어 보관 메모리를 따로 두는 하버드 구조(Harvard Architecture)를 쓰면 해결이 되며, 대다수의 CPU는 메모리 계층에서 Level 1 캐시 메모리를 2개로 나누는 것으로 적용하고 있다.
  • Data Hazard: 파이프라인 내에서 같은 자원을 다루는 명령어가 여러 개 존재한다면 먼저 들어온 명령어를 처리한 결과가 나오기 전까지 나중에 있는 명령어를 처리할 수 없는 문제가 있다. 이것을 데이터 의존성(Data Dependency)라고 하며, 다시 아래의 4가지로 분류된다.
    • Input Dependency: Read-After-Read(RAR) Dependency라고도 하며, 보통은 별도의 대응을 할 필요가 없으나 I/O 작업이 병행되는 특수한 경우(예: Memory Mapped I/O)에는 순서를 지켜서 통신해야 한다. 보통 이 경우에는 Atomic 연산(프로그램 내 특정 연산 구간을 Atomic하게 설정하여 중간에 순서가 뒤섞이는 일이 없도록 CPU에 알림)을 활용하여 순서를 엄격하게 지키게 할 수 있다.
    • Output Dependency: Write-After-Write(WAW) Dependency라고도 하며, 뒤따라온 명령어가 앞선 명령어의 처리 결과를 덮어쓰는 경우다. 레지스터의 이름을 임시로 바꿔서 여러 곳에 결과를 써놓은 다음 나중에 최종 결과를 판별하여 합치는 레지스터 리네이밍 기법으로 어느 정도 대응이 가능하나, 역시 I/O 작업이 병행되는 특수한 경우에는 반드시 순서를 지키도록 강제해야 한다.
    • Anti Dependency: Write-After-Read(WAR) Dependency라고도 하며, 먼저 들어온 명령어대로 메모리에서 읽은 값을 나중에 들어온 명령어가 바꾸는 경우다. 비순차적 실행 기능이 추가된 파이프라인 구조에서는 이것이 문제가 되며, Output Dependency처럼 레지스터 리네이밍으로 해결할 수 있다.
    • Flow Dependency: Read-After-Write(RAW) Dependency라고도 하며, 먼저 들어온 명령어가 값을 쓰고 난 뒤에 나중에 들어오는 명령어가 그 값을 읽는 경우다. 이 경우는 시간여행을 하는 것이 아닌 이상 해결할 방법이 없으며 그에 따라 절대 없앨 수 없는 진정한 의존성이라는 뜻의 True Dependency라는 명칭으로도 불린다.