[3/40] RTL 시뮬레이터 개발일지 – 명령 4개로 나눈 구조

왼쪽에서 들어온 밝은 빛 한 줄기가 네 갈래로 갈라져 뻗어 나가는 그림

vitamin의 명령은 넷이고 처음부터 그렇게 계획했다. 계획은 두 번 어긋났다. 파일 수정 시각(mtime)은 절대 쓰지 않겠다던 규칙을 조건을 붙여 풀었고, 단계별 명령을 만들어 놓고 정작 나는 한 번에 도는 vita만 쓴다.

명령은 처음부터 넷이었다

vitamin에는 명령이 넷 있다.

vita    한 번에 전부
vcmp    컴파일만
velab   엘라보레이트만
vrun    시뮬레이션만

단계별로 하나씩 짓고 그것을 묶는 게 목적이었으니, 묶는 쪽인 vita도 처음부터 설계에 있었다. 3단을 만들어 놓고 불편해서 원샷을 얹은 게 아니다. 적어도 계획은 그랬다.

실행 파일 하나가 불린 이름을 보고 역할을 고른다

넷은 실행 파일 하나다. argv[0]의 베이스네임을 읽어 어떤 애플릿으로 동작할지 고른다. 이름이 vita면 명시적인 서브커맨드 형태도 받는다. 그래서 아래 두 줄은 같은 일을 한다.

Bash
vcmp counter.sv --work work        # vcmp라는 이름으로 불렸을 때
vita vcmp counter.sv --work work   # vita가 서브커맨드를 받았을 때

install.sh는 vita 옆에 나머지 셋의 심볼릭 링크를 걸 뿐이고, 파일시스템이 링크를 거부하면 복사로 대신한다. 링크가 없는 환경에서도 서브커맨드 형태로 전부 쓸 수 있다.

xmvlog를 쓰던 손이 그대로 움직이게

나는 Xcelium을 쓰던 사람이었고, 손에 익은 흐름을 그대로 만들고 싶었다.

하는 일CadenceSynopsysvitamin
컴파일xmvlogvloganvcmp
엘라보레이트xmelabvcsvelab
시뮬레이션xmsimsimvvrun
한 번에xrun–vita

다만 흐름만 가져왔고 기능은 덜어 냈다. Xcelium에 붙어 있는 스케매틱 뷰어나 코드 추적 같은 도구는 vitamin에 전부 없다. 경량 시뮬레이터를 지향했으니 처음부터 목표 밖이었다.

단계마다 끝에 같은 줄이 찍힌다

단계를 가른 실용적인 이유는 어디까지 통과했는지를 각 단계가 스스로 답한다는 데 있다. 셋을 따로 돌리면 이렇게 된다.

Terminal
$ vita vcmp 000_counter.sv --work work
errors=0 warnings=0 notes=0
산출물: work/units/cu_0ac662b75d1112512fd19fc13faad14b.vu

$ vita velab -L work --top tb
errors=0 warnings=0 notes=0
산출물: tb.velab (1,074 B)

$ vita vrun tb.velab
t=16  cnt=1 (0x1)
…
done: final cnt=12
simulation ended (Finish) at time 126
errors=0 warnings=0 notes=0

원샷 vita 000_counter.sv의 출력과 같다. 여기서 봐 둘 것은 cu_0ac662b7….vu라는 중간 산출물 이름인데, 파일 이름 자체가 내용 해시다.

-v를 붙이면 이 실행이 무엇을 먹었는지 찍힌다

-v를 붙이면 트랜스크립트 맨 위에 실제로 적용된 호출이 찍힌다.

Terminal
$ vita -v 000_counter.sv

invocation: vita -v 000_counter.sv
cwd:        /tmp/vita_run
sources:    000_counter.sv
threads:    8 (auto)

지금 찍히는 건 호출, 작업 디렉터리, 실제로 컴파일된 소스 목록, 스레드 수와 그 출처이고, -vv는 자리만 잡아 둔 상태다.

이 기능은 사고가 나서 붙인 게 아니고, 엔지니어로 일하면서 필요하다고 느껴서 미리 넣었다. RTL을 규모 있게 다루면 소스는 filelist로 쪼개져 있고, 합성 흐름으로 넘길 때는 파일마다 타입 같은 것들을 따로 세팅해 줘야 한다. 그때 이 실행이 실제로 무엇을 먹었는지를 도구가 스스로 답해 주면 그 목록을 거기서 만들 수 있다.

소스가 바뀌었는데 vrun만 다시 돌리면

앞 두 단계의 결과가 디스크에 남으니 vrun만 다시 돌리고 싶어진다. 그런데 그 사이에 소스가 바뀌었다면 낡은 스냅샷을 그대로 시뮬레이션해서, 에러 없이 지금 소스와 무관한 결과가 나온다. 내가 가장 피하려는 조용히 틀리는 경우가 여기서 생긴다. 그래서 설계 문서에 규칙(RULE V)을 하나 박아 뒀다.

vrun은 실행할 때마다 앞 단계들이 읽은 소스 파일을 디스크에서 다시 읽어 해시를 계산하고, 스냅샷에 기록된 값과 맞춰 본다. 하나라도 다르면 낡은 스냅샷을 돌리지 않고 실패한다. 파일이 바뀌었는지는 mtime이 아니라 내용 해시로만 판단한다. Xcelium의 -R/-r처럼 이 검사를 건너뛰는 지름길(패스트패스)은 일부러 따라 하지 않는다는 내용까지 규칙에 들어 있다.

파일이 언제 만져졌는지는 내용이 바뀌었다는 증거가 못 된다. 되돌린 편집, 건드리기만 한 파일, 복사로 옮겨 온 트리는 내용이 그대로여도 mtime이 바뀐다. 산출물 이름이 자기 내용의 해시인 것도 이 때문이다.

안 베끼겠다던 mtime 패스트패스를 내가 넣었다

문제는 이 규칙이 비쌌다는 것이다. 시뮬레이션을 돌릴 때마다 소비한 소스를 전부 다시 읽고 다시 해싱하니, 프로젝트가 커질수록 그 비용이 쌓였고 후반으로 갈수록 개발 속도가 눈에 띄게 느려졌다.

2026년 6월에 결국 mtime 패스트패스를 넣었다. 안 베끼겠다고 문서에 적어 둔 바로 그 최적화다. 다만 그냥 넣지는 않았다.

안 베끼겠다던 mtime 패스트패스를 내가 넣었다 도식

velab은 각 경로의 내용을 기록된 해시와 대조해 확인한 뒤에야 (mtime, size)를 스탬프로 남긴다. vrun은 stat 결과가 그 스탬프와 일치할 때만 읽기와 해싱을 건너뛰고, 어긋나거나 스탬프가 없으면 원래대로 전부 다시 해싱한다.

핵심은 스탬프를 언제 찍느냐다. 파일을 쓰는 시점에 mtime을 아무렇게나 잡아 두면 앞 단계와 뒤 단계 사이에 바뀐 파일이 검증받지 않은 채 스탬프를 얻는다. 그래서 내용이 기록된 해시와 같다는 걸 확인한 뒤에만 스탬프를 찍어, 스탬프의 mtime을 그 내용에 묶었다.

적용 범위도 work 라이브러리 모드로 좁혔다. 경로를 명시하는 방식에는 애초에 재해싱이 없어서 해당하지 않는다.

남은 구멍은 문서에 적었다. 파일시스템 타임스탬프 해상도보다 짧은 간격의 변경이나, mtime을 고정한 채 같은 길이로 덮어쓰는 경우처럼 mtime에 표준적으로 알려진 허점은 이 패스트패스를 통과한다.

대가도 있다. 이 (mtime, size) 8바이트 때문에 .velab은 서로 다른 컴파일 실행 사이에서 바이트 단위로 재현되지 않는다. 리눅스와 맥에서 바이트까지 같은 출력을 내겠다는 약속에 문서로 정해 둔 예외가 하나 생긴 것인데, 그 이야기는 결정성을 다루는 편에서 하겠다.

그래서 이걸 원칙을 어겼다고 쓰고 싶지는 않다. 실제로 한 일은 금지를 조건부 예외로 좁히고 그 경계와 비용을 문서에 남긴 것이다. 다만 규칙 문장은 “절대 쓰지 않는다”에서 “이러이러한 조건에서만 믿는다”로 바뀌었다.

로드맵을 보면 이 항목은 18건짜리 성능 백로그에 “매 실행마다 전체 소스를 다시 읽고 다시 해싱함(패스트패스 없음)”이라는 한 줄로, 우선순위 낮음으로 올라 있다가 차례가 와서 처리됐다. 개발이 느려졌다는 내 기억만큼 급한 항목으로 다뤄지지는 않았던 것 같다.

만들어 놓고 정작 나는 vita만 친다

나는 사실상 vita만 쓴다. 원래 의도는 3단 흐름을 각각 독립적으로 구현하고 그것을 한 번에 실행한다는 개념이었는데, 어느 순간부터 단계를 나눠 하나씩 확인하는 대신 원샷 한 줄을 치고 있다.

도구를 만든 사람이 자기가 만든 명령을 안 쓴다는 건 좋게 들리지 않는다. 그런데 지난 편에서 나는 두 경로를 두고 “둘 다 진짜다”라고 썼고, “어느 쪽도 틀리지 않았다. 그저 조금 다를 뿐이다.”라고 덧붙였다. 지금도 둘을 합칠 생각은 없다.

그때 한 말은 두 경로가 똑같이 맞는 답을 낸다는 뜻이었고, 내가 vita만 쓰는 건 평소 습관이라 서로 부딪히지 않는다. 단계별 경로는 필요한 그 순간에 있으라고 있는 쪽이다. 쓰지 않는 걸 유지하려면 이유가 필요하다. 단계가 갈려 있어야 각 단계가 독립적인 모듈로 기능하고, 어디서 깨졌는지를 단계가 스스로 답한다. 무엇보다 도구를 처음 보는 사람에게 중간에 무슨 일이 벌어지는지 보여 줄 수 있다.

단계를 가르면 산출물이 아직 쓸 만한지를 판단해야 하는데, 그 판단의 전체 그림은 결정성과 산출물을 다루는 편에서 이어 간다.

이전 편은 소스에서 파형까지 7단계이고, 다음 편은 산출물 재빌드 판정이다. 연재 전체 목차는 1편에 모아 두었다.

코드는 github.com/tjddnr0912/vitamin-rtl-simulator에 있고 MIT와 Apache-2.0 듀얼 라이선스다. 본문의 실행 기록은 저장소의 examples/000_counter.sv를 릴리스 빌드로 실제로 돌린 결과 그대로다.

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

댓글 남기기

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