[4/40] RTL 시뮬레이터 개발일지 – 산출물 재빌드 판정

어두운 창고의 서류 캐비닛, 서랍 하나만 열려 폴더 하나가 앞으로 나와 있다

vitamin을 단계별로 나눠 돌리면 앞 단계의 산출물이 디스크에 남는다. 소스나 플래그가 바뀌었을 때 그 산출물을 다시 써도 되는지는, 어떤 입력을 판단 근거에 넣느냐로 정해진다.

예전에 시뮬레이션을 돌리면 로그가 주루룩 쏟아지곤 했다. 그런데 같은 걸 다시 돌리면 몇 줄 띡 하고 끝나 버릴 때가 있었다. 그때는 고장 난 줄 알았다.

고장이 아니었다. 바뀐 게 없으니 앞 단계는 건너뛴다고 도구가 판단해서 컴파일 로그가 없었던 것뿐이다.

편리하지만 무서운 기능이다. 건너뛰기가 한 번이라도 잘못 판단하면 도구는 지금 소스와 상관없는 낡은 결과물로 시뮬레이션을 돌리고, 에러 없이 정상 종료한다.

WIDTH=8로 컴파일한 걸 16인 줄 알고 돌리는 사고

어떤 단계를 건너뛰어도 안전하려면, 그 단계의 출력을 바꿀 수 있는 모든 입력이 “이거 아직 쓸 만한가”를 판단하는 값에 들어가 있어야 한다. 하나라도 빠지면 그 하나로 사고가 난다.

가장 알기 쉬운 사고는 이렇다. +define+WIDTH=8로 한 번 컴파일하고, 소스는 그대로 둔 채 +define+WIDTH=16으로 다시 돌린다. 판단 근거가 파일 내용뿐이라면 도구는 바뀐 게 없다고 결론 내고 컴파일을 건너뛴다. 그러면 WIDTH=8로 컴파일된 결과물을 16인 줄 알고 시뮬레이션한다. 에러는 안 나고 파형도 나오는데 숫자만 틀린다.

그래서 매크로 정의는 소스 파일과 함께 판단 근거에 들어가야 한다. 여기까지는 누구나 동의할 것이다. 문제는 나머지 수십 개 플래그다.

어느 명령이 읽느냐 대신 무엇을 바꾸느냐로 나눴다

처음에는 플래그를 vcmp가 받는 것, velab이 받는 것, vrun이 받는 것으로 나누고 싶어진다. 그런데 같은 플래그를 여러 명령이 받을 수 있고, 어떤 명령이 받았는지는 그 플래그가 무엇을 바꾸는지와 별개다. 그래서 어느 단계의 결과를 바꾸느냐로 나눴다.

그러면 버킷이 셋 나온다. 전처리 결과를 바꾸는 A는 컴파일 단계의 판단 근거에, 펼쳐진 설계를 바꾸는 B는 엘라보레이트 단계의 판단 근거에 들어간다. 둘 다 안 바꾸는 C는 로그 파일 이름, 시드, 실행 시간 제한처럼 어떤 판단 근거에도 들어가지 않는 것들이다.

플래그하는 일버킷
+incdir+, -I인클루드 탐색 경로A
+define+, -D매크로 정의A
-y, +libext+라이브러리 탐색, 확장자A
–std, -sv언어 표준 전환A
–timescale시간 단위, 정밀도A와 B
-s, –top-module최상위 모듈 지정B
-G, -P파라미터 오버라이드B
-L, –lib-map라이브러리 결합B
–multi-driver다중 구동 정책B
+plusargs, -sv_seed실행 시 값 전달, 시드C
–log, -o, -Wall로그, 출력 이름, 경고 표시C

훑어보면 설계를 바꾸는 것은 전부 A 아니면 B이고, 결과를 어떻게 보여 줄지에 대한 것은 전부 C다. 파일 목록을 담는 커맨드 파일도 C에 속한다. 소스를 어느 파일에 나눠 적었는지, 그 파일 이름을 뭘로 지었는지는 설계와 무관하니까. 다만 그 안에 적힌 내용은 원래 자리대로 A나 B로 간다.

새 플래그가 전처리 결과를 바꾸는지, 펼쳐진 설계를 바꾸는지 두 번 물어 A·B·C 버킷으로 분류하는 흐름도

새 플래그를 만나면 전처리 결과를 바꾸는지 먼저 묻고, 아니면 펼쳐진 설계를 바꾸는지 묻는다. 둘 다 아니면 C다.

로그 파일 이름까지 넣으면 생기는 일

반대로 판단 근거에 과하게 넣으면 어떻게 될까. 로그 파일 이름을 넣었다고 해 보자. –log a.log로 돌린 다음 –log b.log로 돌리면 도구는 입력이 달라졌다고 판단해서 전체를 다시 컴파일한다. 아무것도 안 바뀌었는데.

같은 결과물을 런타임 옵션만 바꿔서 두 번 돌리면 두 번 다 그대로 쓸 수 있어야 한다. 로그 이름만 바꿨는데 전체를 다시 컴파일하면 단계를 나눈 의미가 절반은 사라진다.

이 규칙은 내가 당해서 만든 게 아니다. 과하게 해싱해서 고생한 기억은 없고 설계하면서 미리 정한 쪽에 가깝다.

표에서 타임스케일만 A와 B 양쪽에 들어가 있다. 전처리 단계에서는 소스에 주입되는 디렉티브라서 컴파일 결과를 바꾸고, 동시에 설계 전체의 시간 정밀도를 정하는 근원이라서 펼쳐진 설계도 바꾼다. 플래그 하나가 두 단계를 다 건드리는 유일한 경우다.

파일 B는 그대로인데 앞의 A가 바뀌었을 때

앞의 것들은 설계하면서 정했다. 다음 것은 실제로 놓쳤다가 나중에 고쳤다.

Verilog에는 파일 하나를 넘어 계속 살아 있는 지시자들이 있다. 타임스케일이나 기본 넷 타입 같은 것들인데, 한 번 선언하면 그 뒤에 오는 파일들에도 계속 적용된다. 그래서 파일 B는 내용이 한 글자도 안 바뀌었는데, 앞에 오던 파일 A가 바뀌면 B의 컴파일 결과가 달라진다. B가 A에게서 물려받던 타임스케일이 달라졌으니까. 파일 순서만 바꿔도 마찬가지다.

판단 근거를 그 파일 자신의 바이트로 잡으면 이 경우가 통째로 새어 나간다. 그래서 두 가지를 고쳤다. 판단 근거를 상속을 반영한 뒤의 내용으로 계산하도록 옮겼고, 정렬된 파일 목록 자체를 별도 판단 근거로 추가해서 파일을 더하거나 빼거나 순서를 바꾸면 그것만으로 무효화되게 했다. 설계 문서에는 이 항목이 지금도 반드시 지켜야 하는 항목으로 표시돼 있다. 놓치면 에러 없이 틀린 결과가 나오기 때문이다.

구조체에 로그 파일 이름을 넣을 자리가 없다

규칙이 이만큼 쌓이면 새 플래그를 추가하는 사람이 이걸 다 기억할지가 걱정된다. 그래서 판단 근거를 계산하는 함수가 명령줄 인자를 통째로 받지 못하고, 필요한 것만 담은 전용 구조체를 받게 만들었다. 전처리용 구조체에는 인클루드 경로와 매크로 정의 같은 것들이, 엘라보레이트용 구조체에는 최상위 모듈과 파라미터 오버라이드 같은 것들이 들어 있다.

로그 파일 이름을 넣을 자리는 그 구조체에 아예 없으니 실수로 넣을 일도 없다. 넣으려면 구조체에 필드를 추가해야 하고, 그건 눈에 띄는 변경이다.

솔직히 이걸 무슨 원칙처럼 의식하면서 짠 건 아니다. 나는 평소에 상황에 맞춰 이것저것 많이 바꾸는 편이다. 다만 옵션이 너무 많아서 뭘 어떻게 설정해야 할지 몰라 헤맨 경험은 확실히 많다. 상용 도구를 쓰다 보면 플래그가 수백 개인데, 그중 어떤 게 결과를 바꾸고 어떤 게 안 바꾸는지 알려 주는 사람이 없다. 그 답답함이 어딘가에 남아 있었던 것 같다. 그러니 이 구조는 규율 있는 개발자가 되자는 다짐보다는, 덜렁대는 나를 전제로 짠 안전장치에 가깝다.

WIDTH를 8에서 16으로 바꿔 두 번 돌려 봤다

앞의 매크로 사고를 그대로 재현해 봤다. 소스는 그대로 두고 +define+WIDTH 값만 바꿔서 두 번 돌린다.

Terminal
$ vita vcmp w.sv --work work +define+WIDTH=8
$ vita velab -L work --top tb
$ vita vrun tb.velab
WIDTH=8   bus=255 (0xff)

$ vita vcmp w.sv --work work +define+WIDTH=16    # 소스는 그대로
$ vita velab -L work --top tb
$ vita vrun tb.velab
WIDTH=16  bus=65535 (0xffff)

제대로 다시 컴파일했다. 매크로가 판단 근거(버킷 A)에 들어가 있으니 바뀐 게 없다고 오판하지 않는다. 이게 빠져 있었다면 두 번째 실행도 255를 찍었을 것이고, 아무 에러 없이 그랬을 것이다.

반대 방향도 확인하려고, 소스를 고친 뒤 재컴파일 없이 예전 .velab을 그대로 실행해 봤다.

Terminal
$ vita vrun tb.velab
error[VITA-E9003] E-ART-STALE-UPSTREAM: w.sv: source changed
  since `vcmp --work` (re-run vcmp + velab)

거부한다. 어느 파일이 바뀌었는지, 무엇을 다시 돌려야 하는지까지 말해 준다.

원샷에는 낡을 산출물이 없다

그런데 지난 편에서 나는 사실상 원샷 명령만 쓴다고 했다. 원샷 실행은 설계를 메모리에 들고 끝까지 가고 디스크에 쓰는 게 없으니, 낡았는지 판단하는 계산을 한 번도 하지 않는다.

이 편의 규칙들과 지난 편의 mtime 지름길은 전부 단계를 나눠 쓸 때만 작동하고, 정작 내 일상 작업에서는 거의 타지 않는다. 그래도 없으면 안 된다. 단계를 나눠 쓰는 사람은 이 규칙 덕분에 낡은 산출물을 피하고, 두 경로가 같은 답을 낸다는 보장도 여기에 걸려 있다.

몇 줄로 끝났던 그 로그로 돌아가서

그때 화면이 조용했던 건 도구가 판단을 내렸기 때문인데, 그 판단이 맞았는지 나는 확인할 방법이 없었고 확인해야 한다는 생각도 못 했다. 지금 만드는 도구에서는 그 근거를 위처럼 적어 두려고 한다.

이 편을 쓰면서 계속 든 생각은, 시작해 보고 나니 예상보다 수십 배는 어렵게 느껴졌다는 것이다. “안 바뀌었으면 다시 안 한다”는 문장은 한 줄인데, 그 한 줄을 안전하게 만드는 데 위의 규칙이 다 필요했다. 캐시가 어렵다는 말은 많이 들었지만 어렵다는 게 이런 모양일 줄은 몰랐다.

이전 편은 명령 4개로 나눈 구조이고, 다음 편은 이벤트 큐와 실행 순서다. 연재 전체 목차는 1편에 모아 두었다.

코드는 github.com/tjddnr0912/vitamin-rtl-simulator에 있고 MIT와 Apache-2.0 듀얼 라이선스다. 본문의 규칙은 저장소 docs/preview/14-staged-artifacts.md의 해시 결합 규칙 절을 풀어 쓴 것이다.

개발 중인 개인 프로젝트의 기록이라 상용 검증 도구를 대체하지 않고, 본문의 구현 범위는 글을 쓴 시점(2026-08-28, 0.2.0, 테스트 6,240개 통과) 기준이라 이후에 바뀐다. 이 프로젝트는 AI와 협업해 개발하고 있다.

댓글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다