---
title: "덧셈 결과는 맞는데 다음 곱셈에서 답이 틀리는 이유"
author: "브레이브브라우저쓰자"
published_at: "2026-10-26T11:58:01.544Z"
updated_at: "2026-10-06T02:34:35.024Z"
tags: ["pipeline ir"]
canonical_url: "https://ariser.io/@writer_a9817d5a8de0/%EB%8D%A7%EC%85%88-%EA%B2%B0%EA%B3%BC%EB%8A%94-%EB%A7%9E%EB%8A%94%EB%8D%B0-%EB%8B%A4%EC%9D%8C-%EA%B3%B1%EC%85%88%EC%97%90%EC%84%9C-%EB%8B%B5%EC%9D%B4-%ED%8B%80%EB%A6%AC%EB%8A%94-%EC%9D%B4%EC%9C%A0"
ai_assisted: true
---

# 덧셈 결과는 맞는데 다음 곱셈에서 답이 틀리는 이유

이 글은 AI가 활용됐습니다.

[원문](https://ariser.io/@writer_a9817d5a8de0/%EB%8D%A7%EC%85%88-%EA%B2%B0%EA%B3%BC%EB%8A%94-%EB%A7%9E%EB%8A%94%EB%8D%B0-%EB%8B%A4%EC%9D%8C-%EA%B3%B1%EC%85%88%EC%97%90%EC%84%9C-%EB%8B%B5%EC%9D%B4-%ED%8B%80%EB%A6%AC%EB%8A%94-%EC%9D%B4%EC%9C%A0)

파이프라인에 레지스터를 추가했는데 결과가 틀리기 시작한다면, 계산식보다 값이 어느 입력에 속하는지 먼저 살펴볼 필요가 있다. 덧셈 결과는 한 사이클 전 입력의 것인데 곱셈에 들어가는 다른 피연산자는 현재 입력의 것일 수 있다. pipeline IR이 stage 경계를 기록하는 이유는 이런 시간의 혼합을 컴파일러가 보지 못한 채 최적화하지 않도록 하기 위해서다.

일반적인 프로그램의 중간 표현에서는 값이 어디에서 정의되고 어디에서 쓰이는지가 핵심이다. 하드웨어 파이프라인에서는 그 사이에 몇 단계의 저장을 거쳤는지도 의미를 갖는다. 식이 같은 두 값이라도 다른 입력의 실행에 속하면 서로 바꿔 쓸 수 없다. 레지스터는 지연을 추가하는 동시에 실행 간 정보를 분리해 보관한다.

CIRCT의 pipeline 설계 설명은 표현을 세 단계로 나눈다. 아직 스케줄되지 않은 계산, stage가 정해졌지만 이동 가능한 표현, 그리고 stage 사이 저장과 전달이 명시된 표현이다. 중간 단계의 `pipeline.src`는 단계 간 사용을 표시해 일반 최적화가 경계를 흐리지 않도록 한다. 마지막 단계에서는 레지스터와 전달할 값이 stage의 인자로 드러난다. [CIRCT, Rationale of the Pipeline Dialect의 Phases 1–3](https://circt.llvm.org/docs/Dialects/Pipeline/RationalePipeline/)

작은 식으로 시간을 추적해 보자. 정수 폭은 모든 예제 값을 담을 만큼 충분하고, 입력은 매 시간 경계에 하나씩 받는다. `s=2*x+b`를 만드는 첫 단계와 `y=s*x`를 만드는 둘째 단계가 각각 한 시간 단위를 쓴다고 가정한다. 첫 단계의 결과를 둘째 단계에 넘길 때 원래 x도 함께 저장해야 한다.

```text
입력 A: x=1, b=10 → s=12 → y=12
입력 B: x=2, b=20 → s=24 → y=48
입력 C: x=3, b=30 → s=36 → y=108
```

A는 시간 0에 첫 단계로 들어가고, 1에 둘째 단계로 넘어가며, 2에 결과를 만든다. B는 1에 들어가 2에 둘째 단계로 넘어가고, 3에 결과를 만든다. A의 s=12가 둘째 단계에서 쓰일 때 그 단계가 받아야 할 x는 1이다. 입력 포트에는 이미 B의 x=2가 보여도 A의 계산에 쓰면 안 된다.

x를 저장하지 않고 현재 포트를 직접 연결하면 A의 결과가 12×2=24로 변한다. 그다음에는 B의 s=24와 C의 x=3을 섞어 72를 만들 수 있다. 덧셈과 곱셈은 각각 정확히 동작해도 전체 출력은 12, 48, 108과 달라진다. stage를 넘는 s와 x가 같은 실행에서 왔다는 관계가 끊긴 것이다. 이 설명은 시간 경계로 정한 가상 스케줄이며 특정 RTL의 nonblocking assignment 실행 순서를 대신하는 설명은 아니다.

데이터와 함께 이동해야 하는 것은 입력 식별에 쓰이는 정보만이 아니다. 유효한 입력이 없는 빈 단계, 즉 bubble도 알아야 한다. A 다음에 빈 입력 구간이 오면 둘째 단계가 빈 구간의 레지스터 비트를 정상 결과로 발표해서는 안 된다. valid가 해당 데이터와 함께 움직여야 한다. 파이프라인이 정지할 수 있다면 데이터와 유효 표시의 정지도 서로 맞아야 한다. 리셋 직후 단계에 남은 비트를 유효한 계산으로 볼지 역시 정해진 초기 상태의 일부다.

다중 사이클 연산이 들어가면 경계의 의미는 더 중요해진다. stage 1에서 시작한 지연 2단계의 연산은 stage 3에서 결과를 쓸 수 있고 stage 2에서는 아직 쓸 수 없다. CIRCT 문서는 이런 결과가 연산기 내부의 시간 지연을 이미 포함한다는 점을 표현에 반영한다. 외부 단계마다 무조건 레지스터를 추가하면 필요한 시점보다 더 늦출 수 있고, 반대로 일반 값처럼 곧바로 사용하면 아직 만들어지지 않은 결과에 의존한다. [CIRCT Pipeline Rationale, Multi-Cycle Operations](https://circt.llvm.org/docs/Dialects/Pipeline/RationalePipeline/)

이 때문에 스케줄이 정해진 뒤의 표현을 곧바로 완전히 고정시키는 것도, 시간 경계를 전부 지워 버리는 것도 각각 비용이 있다. 너무 일찍 고정하면 연산을 앞뒤로 옮겨 더 좋은 단계 배치를 찾기 어렵다. 경계를 지우면 보통의 코드 최적화가 같은 입력의 값이라는 근거 없이 피연산자를 합치거나 옮길 위험이 생긴다. pipeline IR은 변환 단계에 맞는 수준으로 시간 정보를 드러낸다.

여기서 retiming이 가능하다는 말은 어떤 외부 계약도 바꿔도 된다는 뜻이 아니다. 설계가 입력 수락 뒤 정확히 두 사이클에 출력해야 한다면 그 지연이 검증 조건이다. 지연을 바꿀 수 있는 인터페이스라면 수락한 입력과 출력의 대응, 순서, 정지 시 동작을 확인해야 한다. compiler IR에서 허용된 내부 이동과 제품 인터페이스에서 허용된 변경을 함께 정해야 한다.

파이프라인 결과가 틀릴 때는 각 레지스터를 단순한 한 사이클 지연으로 세는 데서 멈추지 말자. 그 레지스터가 어느 입력의 어떤 값을 보관하고, valid와 다른 피연산자는 같은 입력을 가리키는지 적어 보면 좋다. pipeline IR의 stage 경계는 계산을 나누는 선이면서, 서로 다른 실행이 섞이지 않게 해 주는 정보다.

