Zig 빌드 시스템 재작업 내부 살펴보기: 속도, Serialization, 그리고 컴파일러의 진화

Zig 프로그래밍 언어는 코드가 어떻게 실행되는지뿐만 아니라, 어떻게 빌드되는지에 있어서도 시스템 프로그래밍의 경계를 계속해서 넓혀가고 있습니다. 최근 Zig 빌드 시스템의 주요 아키텍처 변화는 단일 구조의 빌드 프로세스에서 분리된 직렬화된 워크플로우로의 전환을 의미합니다. 이러한 변화는 --watch, --fuzz, --webui와 같이 빌드 시스템에 더 많은 기능이 추가됨에 따라, 빌드 로직 자체를 컴파일하는 오버헤드가 개발자 경험에 영향을 미치기 시작했다는 점을 해결하기 위해 설계되었습니다.

아키텍처: Configurer vs. Maker

이전에는 build.zig 파일과 빌드 시스템 구현체가 Debug 모드에서 하나의 거대하고 비대한 프로세스로 컴파일되었습니다. build.zig 로직이 메모리 내에서 빌드 그래프를 구축하는 작업을 마치면, "build runner"가 이를 실행했습니다. 이는 개발자가 zig build를 실행할 때마다 전체 빌드 시스템이 처리되어야 함을 의미했습니다.

이 병목 현상을 제거하기 위해, Zig는 configurermaker를 분리했습니다:

  1. The Configurer: build.zig 파일은 이제 debug 모드에서 작고 가벼운 프로세스로 컴파일됩니다. 이 프로세스의 유일한 작업은 메모리 내에서 빌드 그래프를 구축한 다음 이를 이진(binary) 구성 파일로 직렬화하는 것입니다.
  2. The Maker: configurer가 작동하는 동안, 부모 zig build 프로세스는 Release 모드에서 "maker" 프로세스를 비동기적으로 컴파일합니다. maker는 실제로 빌드 그래프를 실행하는 엔진입니다.

(글로벌 캐시 덕분에) maker 프로세스는 Zig 버전당 한 번만 컴파일되면 되므로, 이는 본질적으로 configurer가 생성한 직렬화된 구성 파일을 소비하는 정적이고 최적화된 이진 파일입니다.

성능 영향

이러한 분리는 세 가지 주요 성능 이점을 제공합니다:

  • 컴파일 오버헤드 감소: 전체 빌드 시스템이 아닌 사용자의 build.zig 로직만 매번 변경 시 컴파일됩니다.
  • 지능적 건너뛰기: 빌드 시스템이 아무것도 변경되지 않았음을 알게 되면(예: -freference-trace와 같은 플래그 추가), build.zig 로직을 완전히 건너뛰고 캐시된 이진 구성 파일을 재사용할 수 있습니다.
  • 최적화된 실행: 빌드 그래프를 실행하는 프로세스(maker)는 이제 모든 최적화가 활성화된 상태로 컴파일됩니다.

영향을 설명하기 위해, Zig 팀은 zig build -h에 대한 벤치마크를 공유했습니다. 실행 시간(wall time)이 150ms에서 14.3ms로 감소하여, 실행 시간이 90.4% 감소했습니다. CPU 사이클과 명령어 수도 유사하게 급격히 감소했습니다(95% 이상).

변경 사항 및 툴링

이번 재작업은 API 관점에서 대체로 비파괴적(non-breaking)이지만, 빌드 스크립트가 인자를 처리하는 방식에 한 가지 주목할 만한 변화가 있습니다. b.args를 관찰하던 이전 방식은 다음과 같이 대체되었습니다:

이전 방식:

if (b.args) |args| {
    run_cmd.addArgs(args);
}

새로운 방식:

run_cmd.addPassthruArgs();

빌드 스크립트가 이러한 인자를 직접 관찰할 수 있는 기능을 제거함으로써, Zig는 인자를 변경하더라도 빌드 스크립트를 소스에서 다시 빌드할 필요가 없도록 보장하며, 이를 통해 루프를 더욱 가속화합니다.

성능을 넘어, 이 변화는 서드파티 툴링에도 이점을 줍니다. ZLS (Zig Language Server)와 같은 도구들은 이제 빌드 러너의 복잡한 포크(fork)를 유지 관리할 필요 없이 직렬화된 구성 파일을 직접 소비할 수 있습니다.

2026년의 더 넓은 컴파일러 진화

빌드 시스템 재작업은 2026년 내내 Zig 에코시스템 전반에서 일어나는 최적화 및 개선이라는 더 큰 흐름의 일부입니다. 다른 몇 가지 주요 업데이트가 프로젝트의 방향성을 보여줍니다:

증분 컴파일 및 타입 해석

Zig는 LLVM 백엔드를 위한 증분 컴파일을 도입하여, 에러를 보고할 때 컴파일러 코드에서 소요되는 시간을 크게 줄였습니다. 또한, 내부 타입 해석 로직의 대대적인 재설계(30,000라인 규모의 PR)를 통해 컴파일러를 더 "게으르게(lazier)" 만들었습니다. 이제 더 이상 초기화되지 않은 타입의 필드를 분석하지 않으므로, 타입이 불필요한 컴파일 에러를 유발하지 않고 네임스페이스 역할을 겸할 수 있게 되었습니다.

I/O 및 "약속된 땅"

Zig는 I/O 구현을 손쉽게 교체할 수 있는 미래를 향로하고 있습니다. std.Io.Evented의 도입은 개발자가 핵심 애플리케이션 로직을 변경하지 않고도 스레드 기반 I/O와 이벤트 기반 I/O(Linux의 io_uring 또는 macOS의 Grand Central Dispatch 사용) 사이를 전환할 수 있게 해줍니다. 이는 유저스페이스 스택 스위칭(fibers/green threads)을 통해 달성됩니다.

Windows 네이티브 API 선호

비대함(bloat)을 줄이고 신뢰성을 높이기 위해, Zig는 Win32 (kernel32.dll) 래퍼 대신 네이티브 NT API를 선호하도록 표준 라이브러리 정책을 변경하고 있습니다. 이를 통해 이러한 래퍼들을 우회함으로써, Zig는 특히 엔트로피 생성 및 파일 I/O에서 불필요한 힙 할당과 문서화되지 않은 실패 모드를 피할 수 있습니다.

커뮤니티 관점

이러한 변화에 대한 커뮤니티의 반응은 대체로 긍정적이며, 사용자들은 언어의 "tinker-friendly"한 특성을 찬양합니다. 한 사용자는 Zig가 휴식 후 다시 시작하기에 배우기 쉽고 현대적인 툴링을 갖춘 언어로서 "sweet spot"에 가깝다고 언급했습니다. 하지만 일부 사용자들은 언어의 현재 불안정성에 대해 우려를 표하며, async I/O와 같은 주요 기능의 빈번한 breaking changes가 안정적인 프로덕션 환경으로 넘어가기 전에 주의가 필요한 이유라고 언급했습니다.

Zig가 버전 0.17.0을 향해 나아갈 때, 이러한 아키텍처적 변화는 이 언어가 점점 더 자립적인(self-reliant) 언어로 변모하고 있음을 시사합니다. 즉, C와 서드파티 DLL에 대한 의존성을 줄이면서, 빌드에 사용되는 도구 자체를 최적화하고 있습니다.

Sources