← На главную

Zig получил инкрементальную компиляцию: правки за 50–70 мс

28.07.2026 15:46 · hackernews

Zig-компилятор получил работающую инкрементальную компиляцию, которая превращает правки кода в пересборку за десятки миллисекунд. Участник core-команды Zig показал пример на графическом редакторе Fizzy: первая сборка занимает 5 секунд, а каждое последующее изменение — 50–70 мс. В Zig 0.16.0 поддержка уже была, но для стабильной работы нужны свежие фиксы линкера, поэтому придётся либо взять master-ветку, либо ждать 0.17.0.

Первый этап — обработка исходников. Компилятор читает файл, строит AST и прогоняет через AstGen, который выдаёт ZIR (Zig Intermediate Representation) — нетипизированное SSA-представление. Обработка каждого файла чистая и нешарит состояние, поэтому её легко параллелить. ZIR умеет писаться и читаться с диска одним системным вызовом writev/readv, без отдельной сериализации. Компилятор годами кеширует ZIR каждого файла и перестраивает его, только если исходник изменился; этап «AST Lowering» пролетает почти мгновенно.

Семантический анализ — самое сложное. Здесь программа разбивается на юниты анализа: раскладка struct/union, тип объявления, значение константы, известной на этапе compile-time, и тело runtime-функции. При анализе юнита собираются зависимости от других юнитов и от конкретных кусков исходного кода (через хеши). Если меняется хеш региона, инвалидируется юнит и по цепочке переанализируются все зависимые юниты. Зависимости от тела runtime-функции невозможны, поэтому каскад не уходит в бесконечность.

Кодогенерация превращает AIR — результат семантики — в MIR (почти машинный код) и тоже параллелится. Кешировать AIR или MIR не нужно, потому что гранулярность совпадает с инкрементальной штукой. Самое интересное — линковка. Инкрементальная линковка сложна, универсальных инкрементальных линкеров нет: даже wild Дэвида Латтимора сместил фокус на холодную скорость. Решение Zig — тесно интегрированный линкер с абстракцией link.MappedFile, которую ввёл Jacob Young. Выходной бинарник маппится в память как дерево узлов. Когда генерируется машинный код функции, ей выделяется (или расширяется) узел прямо в mapped-файле. Если места не хватает, MappedFile двигает соседние узлы и помечает их dirty. На холостом ходу линкер обрабатывает грязные узлы: обновляет адреса, таблицу символов и применяет релокейшены. Экспоненциальный рост узлов делает перемещения редкими.

В конце компилятор обходит граф ссылок, чтобы понять, какие объявления реально используются, и делает flush — записывает секцию .dynamic и entry point в ELF-заголовок, минимизируя финальную работу.

Профилирование Tracy показало, что из 37 мс инкрементального обновления около 31 мс уходит на обход графа ссылок (resolveReferencesInner), даже если он не изменился. Семантика, кодоген и линковка занимают всего 1.6 мс. Это оставляет большой запас для будущих оптимизаций.

Попробовать можно командой zig build --watch -fincremental, но пока только на x86_64-linux. Кеш на диске не совместим с обычным режимом, поэтому в build.zig можно включить инкрементальность только для конкретного артефакта.

Читать оригинал →