컴파일러는 결정론적인가?
I’m happy to translate the article for you, but I need the full text of the post (the part after the source line) in order to do so. Could you please paste the content you’d like translated?
22 Feb 2026
Betteridge는 “no”라고 말하고, 일반적인 개발자 경험에서는 그 답이 대부분 맞습니다. (또한, 당신 말이 전적으로 맞아요! — 이 글을 쓰는 데 ChatGPT를 사용했습니다.)
내 생각
두 가지 보완적인 답변이 있습니다:
- 컴퓨터‑과학 답변 – 컴파일러는 전체 입력 상태의 함수로서 결정적입니다.
- 엔지니어링 답변 – 대부분의 실제 빌드는 전체 입력 상태를 제어하지 않으므로 출력이 시간이 지남에 따라 변동합니다.
Formal model
artifact = F(
source,
flags,
compiler binary,
linker + assembler,
libc + runtime,
env vars,
filesystem view,
locale + timezone,
clock,
kernel behavior,
hardware / concurrency schedule
)
실제로 대부분의 팀은 source(및 경우에 따라 flags)만을 고정하고 나머지는 모두 “노이즈”라고 라벨링합니다.
그 “노이즈”가 바로 비재현성이 존재하는 곳입니다.
Ksplice에서 배운 교훈
나는 2000년대에 Ksplice에서 근무했으며, 실행 중인 Linux 커널을 RAM에 패치하여 재부팅 없이 보안 업데이트를 적용했습니다.
크래시가 발생하기 쉬운 커널의 objdump 출력을 보는 것이 일상은 아니었지만, 충분히 자주 발생했기 때문에 컴파일러 출력과 소스 의도 사이의 차이가 이론적인 것이 아니라는 것을 깨달았습니다.
- 우리는 이전과 새로운 컴파일된 객체를 diff하고, 핫 패치를 실시간 메모리에 삽입함으로써 재부팅이 필요 없는 커널 업데이트를 생성했습니다.
- 대부분의 diff는 변경된 C 코드와 깔끔하게 매핑되었습니다.
- 가끔씩 소스 의미와 무관한 이유—레지스터 할당 차이, 패스 동작 변경, 혹은 섹션/레이아웃 변화— 때문에 “폭발”하기도 했습니다.
- 같은 의도이지만, 생성된 머신 코드는 달랐습니다.
구체적인 역사적 사례
GCC bug 18574는 순회 순서와 SSA 결합에 영향을 미친 포인터‑해시 불안정성에 대해 논의합니다. 이 스레드는 도구 체인에서 겉보기에 사소한 변경이 재현성을 깨뜨릴 수 있음을 보여줍니다.
주요 구분
| 개념 | 정의 |
|---|---|
| Deterministic compiler | 정확히 동일한 전체 입력 튜플이 주어지면 항상 같은 출력을 생성합니다. |
| Reproducible build | 두 개의 독립적인 빌더가 비트‑동일인 아티팩트를 재현할 수 있습니다. |
| Reliable toolchain | 출력 차이는 드물며, 발생하더라도 기능적 정확성에 영향을 미치는 경우는 거의 없습니다. |
이러한 개념들은 서로 관련이 있지만 동등하지 않습니다; 차이를 이해하면 빌드 안정성에 대한 현실적인 기대치를 설정하는 데 도움이 됩니다.
컴파일러 계약: 의미론, 바이트 정체성 아님
댓글 작성자가 이 점에 대해 옳습니다: 컴파일러는 의미론을 보존해야 합니다. 정의된 동작을 가진 프로그램의 경우, 출력은 소스 언어의 추상 머신과 관찰적으로 동등해야 합니다.
즉, 명령어 순서, 레지스터 선택, 인라인 전략, 블록 레이아웃은 모두 자유롭게 변경될 수 있습니다—외부에서 보이는 동작이 동일하게 유지되는 한. 실제로 “보이는 동작”에는 다음과 같은 것들이 포함됩니다:
- I/O 효과
- 휘발성 접근
- 원자적‑동기화 보장
- 정의된 반환값
…하지만 바이트‑대‑바이트 명령어 정체성은 포함되지 않습니다.
중요한 주의사항
- 정의되지 않은 동작은 의미론적 보장을 약화시키거나 무효화합니다.
- 타이밍, 마이크로아키텍처 측면 채널, 정확한 메모리 레이아웃은 일반적으로 핵심 언어 계약의 범위를 벗어납니다.
- 재현 가능한 빌드는 의미론 보존보다 더 엄격한 목표이며(동일한 비트가 필요함), 단순히 동일한 동작만을 요구하는 것이 아닙니다.
엔트로피가 발생하는 곳
__DATE__,__TIME__,__TIMESTAMP__- DWARF/디버그 정보에 포함된 절대 경로
- 빌드 경로 누출 (예:
/home/fragmede/projects/foo) - 로케일에 민감한 정렬 동작 (
LC_ALL) - 파일 시스템 순회 순서
- 병렬 빌드 및 링크 레이스 순서
- 아카이브 멤버 순서와 메타데이터 (
ar,ranlib) - 빌드 ID, UUID, 랜덤 시드
- 빌드 중 네트워크 가져오기
- 툴체인 버전 차이
- 호스트 커널 / C 라이브러리 차이
- 불안정한 포인터 또는 해시 순회 순서에 의존하는 과거 컴파일러 내부 구조
ASLR 참고: ASLR은 직접적으로 배출된 바이너리를 무작위화하지 않으며, 프로세스 메모리 레이아웃을 무작위화합니다. 그러나 컴파일러 패스의 동작이 포인터 정체성이나 순서에 의존한다면, ASLR은 간접적으로 결과에 영향을 줄 수 있습니다.
따라서 “컴파일러는 결정론적이다”는 말은 이론적인 의미에서는 종종 맞지만, 실제 운영 환경에서는 틀릴 수 있습니다.
재현 가능한 아티팩트가 있더라도 Ken Thompson의 Reflections on Trusting Trust 은 여전히 적용됩니다.
컴파일러는 새로운 기술이 아니라는 점을 기억하세요: Grace Hopper의 A‑0 system 은 1952년 UNIVAC I에서 시작되었습니다. (ChatGPT는 겨우 4년 정도 되었지만, 컴파일러는 74년 동안 존재해 왔습니다.)
재현 가능한 빌드: 의도적인 엔지니어링
Debian 및 더 넓은 reproducible‑builds 노력(2013년 이후)에서 이 주류 아이디어를 추진했습니다: 같은 소스 + 같은 빌드 지침이 비트‑대‑비트 동일한 아티팩트를 생성해야 한다.
실용적인 플레이북
- 툴체인 및 의존성 고정
- 안정적인 환경 사용 – 예:
TZ=UTC,LC_ALL=C - 고정된 타임스탬프를 위해
SOURCE_DATE_EPOCH설정 - 휘발성 메타데이터 정규화 / 제거 (타임스탬프, ID 등)
- 경로 프리픽스 정규화
-ffile-prefix-map= = -fdebug-prefix-map= = - 결정론적 아카이브 생성 – 예:
ar -D - 빌드 그래프에서 네트워크 접근 차단
- 헐멧 컨테이너 또는 샌드박스에서 빌드
- CI에서 빌더 간 아티팩트 지속적으로 diff
결과
- 반복 가능 – 같은 명령을 실행하면 같은 결과를 얻음.
- 재현 가능 – 서로 다른 머신에서도 동일한 바이너리를 생성.
- 검증 가능 – 누구나 출력이 소스와 일치하는지 확인 가능.
- 헐멧 – 빌드가 외부 상태와 격리됨.
- 결정론적 – 숨겨진 무작위성이 결과에 영향을 주지 않음.
현재 상황
우리가 지금 이걸 가지고 있나요?
많은 생태계에서 답은 대부분 예이지만, 컴파일러, 링커, 패키징 도구, 빌드 시스템 전반에 걸친 수년간의 의도적인 작업이 필요했습니다. 우리는 복잡한 엣지 케이스들을 하나씩 해결하면서 여기까지 왔으며, 단순히 손을 흔들며 순수성을 선언한 것이 아닙니다.
왜 이것이 LLM에 중요한가
질문은 종종 다음과 같이 제기됩니다: “LLM이 비결정론적이라면 vibecoding은 타당한가?”
답변하기 전에 컴퓨터 과학 관점인지 공학 관점인지 결정하세요.
정지 문제 비유
- 우리는 형식적이고 이론적인 의미에서 정지 문제를 해결하지 못했습니다.
- 실제로는, LLM이 깨진
for‑루프를 찾아내고, 조건이 잘못된 이유를 설명하며, 심지히 수정안을 제시할 수 있습니다.
공학적 현실
공학은 절대 완전히 결정론적인 지능에 의존하지 않습니다.
대신, 다음에 의존합니다:
- 제어된 인터페이스
- 테스트 오라클
- 재현 가능한 파이프라인
- 관측성
나는 매일 comma.ai 차를 탈 정도로 “AI‑에 중독”돼 있지만, 생성된 코드 주변에 결정론적인 검증 게이트를 여전히 요구합니다.
내 여자친구는 차가 더 부드럽고 덜 불규칙적인 동작을 선호합니다—이는 확률적 시스템도 운영상으로 더 나은 결과를 제공할 수 있음을 상기시켜 줍니다.