가변 latency 연산기가 고정 파이프라인에 들어올 때
고정된 몇 단계의 파이프라인에 나눗셈 연산기를 붙였다. 어떤 입력에서는 빨리 끝나고 어떤 입력에서는 오래 걸린다. 원래의 writeback 시점에 결과를 읽으면 아직 계산 중이고, 최악의 시간을 기다리면 짧은 작업도 늦어진다. 가변 latency 연산기는 특정 사이클에 결과가 올 것이라는 약속 대신, 준비와 완료를 알려 주는 계약으로 연결해야 한다. latency는 요청 수락에서 결과 준비까지 걸리는 시간이다.
여기서 중요한 것은 연산을 빨리 구현하는 방법보다, 완료 시점을 모르는 블록의 책임을 파이프라인이 어떻게 관리하는가다. 같은 연산기를 사용해도 전체 정지 방식과 독립 완료 방식의 제어 비용은 크게 다르다.
가장 단순한 답은 현재 명령을 붙들기
명령을 순서대로 한 개씩 처리하는 실행 단계라면, 연산기를 시작한 뒤 완료까지 현재 명령과 피연산자 정보를 유지할 수 있다. 앞 단계에는 stall을 보내고, 완료가 오면 결과를 기록한 뒤 다음 명령으로 넘어간다.
이 구조의 장점은 결과가 어느 명령에 속하는지 자연스럽게 알 수 있다는 것이다. 현재 명령이 하나뿐이다. 한 요청을 여러 번 시작하지 않도록 start를 수락 사건으로 만들고, 계산 중에는 재발행하지 않으면 된다. 완료 신호와 결과의 유효성이 맞아야 하는 조건도 명확하다.
Ibex의 pipeline 문서는 기본 두 단계 구성에서 여러 사이클 명령이 ID/EX 단계를 완료까지 멈추게 하는 방식을 설명한다. 문서가 별도로 언급하는 세 단계 옵션과 섞지 않아야 한다. 이 사례는 전체 정지가 실패한 구조가 아니라, 단순한 실행 계약을 선택한 구조임을 보여 준다.
단점은 뒤의 독립적인 작업도 기다린다는 것이다. 나눗셈 결과를 사용하지 않는 덧셈이라도 같은 실행 흐름에서 앞 명령을 넘길 수 없다. 비용은 연산기의 긴 latency 자체에 더해, 그 시간 동안 가능한 다른 작업을 하지 못하는 데서 생긴다.
연산기만 바쁘게 두면 필요한 정보가 늘어난다
대신 나눗셈기를 시작하고 다른 독립 작업을 진행시키면 어떨까. 가상의 명령 셋을 생각하자. 숫자는 설명용이며 실제 CPU의 사이클 수가 아니다.
I0는 나눗셈이고 에지 0에 수락되어 에지 6에 결과가 준비된다. I1은 I0와 무관한 덧셈이며 에지 1에 시작해 에지 2에 완료된다. I2는 I0의 결과를 사용하는 연산이다.
독립 실행을 허용하면 I1은 I0보다 먼저 완료될 수 있다. 하지만 I2는 I0의 결과가 준비되기 전에 시작하면 안 된다. 수락 가능한 연산기가 있다는 사실과 피연산자가 준비되었다는 사실을 함께 확인해야 한다.
assign issue_div = is_division
&& operands_ready
&& divider_ready
&& completion_slot_available;
이 식은 발행 조건을 보여 주는 설명용 조각이다. 실제 issue_valid의 유지와 수락 사건은 선택한 인터페이스 규칙으로 구현해야 한다. divider_ready는 새 요청을 받을 수 있다는 뜻이고, completion_slot_available은 완료 결과를 잃지 않고 보관할 수 있다는 뜻이다. 두 조건은 다른 자원을 가리킨다.
I0와 I1의 완료가 순서대로 오지 않으므로 요청 식별자를 결과와 함께 유지해야 한다. 목적지 레지스터나 결과 슬롯을 기억하는 메타데이터가 계산 경로와 같은 작업에 붙어 있어야 한다. 완료 시점에 현재 실행 중인 명령의 목적지를 그대로 사용하면 I0 결과를 I1이나 I2의 자리에 쓸 수 있다.
완료 순서와 외부에 보이는 순서
독립적으로 계산을 끝내는 것과 프로그램의 상태를 그 순서로 확정하는 것은 다르다. 프로그램이 순서대로 실행한 것처럼 보이도록 해야 한다면, I1의 결과를 잠시 보관하고 I0가 정상 완료된 뒤 상태를 확정할 수 있다.
가령 단일 commit 포트를 사용하고 I0를 에지 6에 확정한 뒤 I1을 다음 에지에 확정한다고 정하면, I1은 에지 2에 계산을 끝내고도 에지 7까지 기다린다. 이 일정은 예제를 위한 정책이다. 계산 완료와 최종 반영의 사이에 결과가 남아 있어야 한다는 사실을 보여 준다.
BOOM의 실행 파이프라인 문서는 준비 상태를 바꾸는 비파이프라인 연산기와, 알려진 고정 latency로 작업을 받는 연산기를 구분한다. ROB 문서는 진행 중인 명령의 완료와 순서대로 이루어지는 commit을 나누어 설명한다. 이런 구조는 가변 완료 시간을 없애는 대신, 그 불확실성을 추적할 상태를 제공한다.
결과가 올 때 자리가 없으면
연산기가 요청을 받은 뒤 멈출 수 없다면, 완료할 때 결과를 수락할 공간이 보장되어야 한다. 다른 연산들이 같은 writeback 포트를 쓰고 있거나 하류가 막히면 완료 결과가 한 사이클 펄스로 지나갈 수 있다. “언젠가 ready가 올라오겠지”라는 기대는 저장을 대신하지 못한다.
연산기가 결과 valid와 데이터를 수락까지 유지할 수 있다면 완료 포트에서 기다릴 수 있다. 그렇지 않다면 결과 버퍼가 필요하거나, 발행할 때 미래의 저장 공간을 예약해야 한다. 고정 완료 시간일 때 가능했던 특정 사이클의 포트 예약도 가변 완료에서는 충돌을 예상하기 어려워진다. 동적 중재와 보관이 필요한 이유다.
원문에서 비파이프라인 연산기가 busy 동안 ready를 내리는 것은 한 작업이 연산기를 차지한다는 뜻이다. 이것이 CPU 전체가 반드시 멈춘다는 뜻은 아니다. 해당 연산기를 기다리는 작업만 막고 다른 자원의 작업은 진행시키는 구조를 선택할 수 있다. 반대로 여러 사이클 latency를 갖는 연산기라도 내부 파이프라인이 새 작업을 매 사이클 받을 수 있다면 요청 준비 규칙은 달라진다. latency만으로 시작 간격을 추측하면 안 된다.
취소와 의존성 해제를 맞추기
오래 걸리는 동안 분기 오류나 예외로 I0가 취소될 수 있다. 완료를 버리는 것으로 끝내면 I0의 목적지가 여전히 busy로 남아 I2가 기다릴 수 있다. 반대로 취소한 I0의 늦은 결과가 새 명령의 목적지를 ready로 만들면 잘못된 데이터로 실행을 시작할 수 있다.
따라서 완료 식별과 취소 상태를 함께 확인하고, 목적지 준비와 예약 반환도 해당 작업에 맞게 처리해야 한다. BOOM 문서가 비파이프라인 연산기에 잘못 추측한 작업을 빠르게 제거하는 kill 지원을 요구하는 것도 이런 통합 조건의 한 부분이다. 실제 연산기가 즉시 중단되는지, 계산은 계속하고 결과만 억제하는지는 구현에 따라 다르다.
처음의 고정 writeback 시점은 가변 latency 연산기에 더 이상 유효한 약속이 아니었다. 전체를 멈추면 단순한 책임을 유지할 수 있고, 독립 실행을 허용하면 완료 식별과 의존성, 결과 보관 및 취소 상태를 추가해야 한다. 가변 latency 연산기를 잘 연결했다는 기준은 가장 짧은 입력이 빨리 끝난다는 사실보다, 어떤 시점에 끝나도 올바른 작업으로 정확히 한 번 회수할 수 있다는 보장이다.