Блог
[один M1 Pro, одна синтетическая программа; цифры t27b сняты при load average 83–165 на 8 ядрах; t27b понимает 37 из 1174 спек, а его JIT работает только на arm64 macOS; три из четырёх найденных им ошибок ещё не исправлены] t27c проверяет типы одной функции за 29,9 мкс, в 5,2 раза быстрее rustc check, но машинного кода не выдаёт, поэтому его полный путь до объектного файла в 2,1 раза медленнее clang на той же программе, написанной на C. Чистая release-сборка самого t27c сократилась со 142,7 с до 66,0 с после трёх влитых PR, убравших неиспользуемые библиотеки машинного обучения и сборку aws-lc-sys. t27b, собственный бэкенд AArch64 без LLVM, уже влит: он компилирует и запускает те же тесты в 23–381 раз быстрее пути через Zig, пишет код примерно уровня clang -O0 и нашёл четыре ошибки в t27c и в одной спеке.
t27c, компилятор t27, — транслятор. Он читает спеку .t27 и печатает Zig, C, Rust или Verilog, а машинный код из этого делает другой компилятор, почти всегда LLVM. Здесь три вопроса. Насколько быстра та часть, которую t27c делает сам? Сколько стоит весь путь, если считать бэкенд? И сколько собирается сам t27c — ведь это программа на Rust? В конце — наш собственный маленький бэкенд t27b, который выдаёт код AArch64 без LLVM, и четыре ошибки, которые он нашёл по дороге.
Всё замерено 4 октября 2026 года на одном Apple M1 Pro (8 ядер, 16 ГБ). Тестовая программа синтетическая. Ограничения перечислены в конце, и они существенны.
Жалоба старше Rust. Go в Google придумывали в том числе потому, что большая сборка на C++ шла 45 минут. По рассказу Пайка 2012 года, 4,2 МБ исходников на C++ после раскрытия всех #include превращались больше чем в 8 ГБ — примерно в 2000 раз больше. Наш тестовый файл показывает то же в малом: шесть стандартных заголовков после clang++ -E превращают 1007 строк в 88 104.
У Rust цена другая. Обобщённый код компилируется заново для каждого конкретного типа, а единица компиляции — целый крейт. Чтобы занять ядра, rustc делит крейт на единицы кодогенерации (CGU), но один модуль не делит никогда. На t27c это видно напрямую. bootstrap/src/compiler.rs — 44 636 строк в одном модуле, его CGU весит 880 КБ, и 98,5% из них — этот один файл. По -Z time-passes на release-сборке крейта t27c rustc 36,6 с из 60,7 с ждал LLVM. Машина во время этого прогона была нагружена.
Дальше сам LLVM. В нашем тесте фронтенд clang (-fsyntax-only) занимает 24,5% времени clang -O0. Остальное — IR, кодогенерация LLVM и запись объектного файла. Оптимизации стоят ещё больше: -O2 идёт в 6,4 раза дольше -O0 у clang на C и в 3,7 раза дольше у rustc. Нагляднее всего Zig. Со своим бэкендом (-fno-llvm) он собирает тот же файл в 5,7 раза быстрее, чем через LLVM. Опубликованные бэкенды без LLVM дают выигрыш того же порядка: Copy-and-Patch (OOPSLA 2021) компилирует примерно в 100 раз быстрее LLVM -O0, TPDE (CGO 2026) — в 8–24 раза.
Об одном пробеле в литературе стоит сказать прямо. Рецензируемой работы, которая технически измеряет время компиляции Rust, мы не нашли. В опросе проекта Rust о скорости компилятора 2025 года больше 3700 ответов, и 55% опрошенных ждут инкрементальную пересборку дольше 10 с. Это пост в блоге, а не статья. Все цифры о Rust ниже — наши собственные замеры.
N независимых функций, в каждой цикл на 8 итераций над u32. В версии на t27 у каждой функции ещё и свой блок test. Та же программа написана вручную на C, C++, Rust, Zig, Go и Swift, для N = 100, 1000 и 5000.| Компилятор | Что делает | мс при N = 5000 | мкс на функцию |
|---|---|---|---|
clang -fsyntax-only (C) | разбор и проверка C, без кода | 121,9 | 24,4 |
t27c typecheck | разбор и проверка типов t27, без кода | 149,7 | 29,9 |
t27c gen-c | разбор, проверка и печать исходника на C | 168,4 | 33,7 |
zig -fno-llvm | собственный бэкенд Zig, Debug, без LLVM | 410,3 | 82,1 |
go tool compile | собственный бэкенд Go | 476,1 | 95,2 |
clang -O0 (C) | .o через LLVM без оптимизаций | 498,1 | 99,6 |
rustc check | типы и заимствования, только метаданные | 772,1 | 154,4 |
rustc -O0 | .o через LLVM без оптимизаций | 1457,8 | 291,6 |
zig Debug (LLVM) | .o через LLVM, Debug | 2341,9 | 468,4 |
clang -O2 (C) | .o через LLVM, -O2 | 3180,2 | 636,0 |
rustc -O2 | .o через LLVM, opt-level=2 | 5447,6 | 1089,5 |
swiftc -typecheck | проверка типов Swift, без кода | 10 423,2 | 2084,6 |
Среди тех, кто только проверяет код, t27c typecheck на втором месте: 29,9 мкс на функцию против 24,4 у clang. Это в 5,2 раза быстрее rustc check и в 69,6 раза быстрее swiftc -typecheck. Печать исходника на C поверх проверки стоит немного: t27c gen-c — 33,7 мкс.
Поэтому честное сравнение — весь путь: t27c плюс бэкенд, который работает с тем, что t27c напечатал.
| Путь, N = 5000 | t27c, мс | бэкенд, мс | всего, мс | доля t27c |
|---|---|---|---|---|
| C, написанный сразу: clang -O0 | — | 498,1 | 498,1 | — |
| t27 → C → clang -O0 | 168,4 | 899,6 | 1068,0 | 15,8% |
| Zig, написанный сразу: zig Debug (LLVM) | — | 2341,9 | 2341,9 | — |
| t27 → Zig → zig test (сборка и линковка) | 167,9 | 3370,4 | 3538,4 | 4,7% |
Самый быстрый путь t27 до машинного кода идёт через C: 1068,0 мс. Та же программа, написанная на C вручную, собирается за 498,1 мс, в 2,1 раза быстрее. Большая часть разницы — не t27c. Сгенерированный C — 80 043 строки против 50 001, потому что в нём ещё 5000 тестов. В пути через Zig на бэкенд уходит 95,3% времени. Пока t27c зависит от чужого бэкенда, его полный путь никогда не быстрее этого бэкенда.
t27c — программа на Rust, и всё сказанное в первом разделе относится и к нему. Чистая release-сборка шла 142,7 с по часам. Отчёт cargo о времени показывает, куда оно уходило:
| Единица сборки | Версия | Секунды | Судьба |
|---|---|---|---|
aws-lc-sys (build script) | 0.41.0 | 78,8 | убрано в #5920 |
candle-core | 0.11.0 | 52,4 | убрано в #5900 |
candle-core | 0.10.2 | 49,4 | убрано в #5900 |
t27c (сам компилятор) | 0.4.0 | 34,4 | осталось |
tokenizers | 0.22.2 | 33,3 | убрано в #5900 |
candle-nn | 0.10.2 | 14,6 | убрано в #5900 |
Самая долгая единица — сборочный скрипт aws-lc-sys, криптобиблиотеки на C, которая приходила через reqwest → rustls → aws-lc-rs. Дальше две версии candle-core и tokenizers — библиотеки машинного обучения, которые в bootstrap/src нигде не использовались. Сам крейт t27c — 34,4 с. Большую часть этого убрали три влитых PR:
candle-core и candle-nn. Сборка: 142,7 с → 97,9 с, единиц сборки 386 → 279. Набор падающих тестов t27c suite до и после один и тот же.reqwest, tokio, axum, hyper, jsonwebtoken) за cargo-фичами net и server. Сборка по умолчанию теперь компилирует 49 крейтов вместо 212.reqwest на native-tls, и сборка aws-lc-sys исчезла. Сборка с сетевым стеком: 97,9 с → 66,0 с, на 54% меньше исходной.Чистая сборка бывает редко, а правка одной строки и пересборка — постоянно. Обычная release-пересборка после правки одной строки шла 29,8–33,9 с: release не инкрементальный, и большая единица compiler.rs стоит на критическом пути. С CARGO_PROFILE_RELEASE_INCREMENTAL=true — 3,2–5,0 с, а cargo check — 2,4–3,3 с. #5945 (влит) записал этот цикл в CONTRIBUTING.
Одна идея не сработала. Мы разбили compiler.rs на 24 файла, и вывод компилятора остался тем же побайтно. Потом собрали крейт t27c 6 раундами вперемешку. Медиана сдвинулась с 49,6 с до 37,8 с, минимум — с 34,1 с до 32,1 с, а процессорное время выросло с 95,9 с до 98,4 с. Разбиение было быстрее только в 3 парах из 6, а отдельные сборки шли от 32 до 117 с. Это шум, а не ускорение. Ветка остаётся локальной и PR не стала.
t27b — отдельный крейт cli/t27b, влит в #5979 (следом влит #5989, который вписал его в CI-реестр крейтов-сирот). Он сам выдаёт машинный код AArch64, без LLVM, zig, clang и rustc. Фронтенд у него — разборщик и проверка типов самого t27c, подключённые как есть. Дальше код новый: своё промежуточное представление, кодировщик команд A64, а затем либо исполнение в памяти (t27b test, JIT), либо объектный файл Mach-O (t27b build). Переполнение целых определено: ловушка или перенос (--overflow trap|wrap), а сдвиг на величину вне [0, ширина) — ловушка. Эталон для проверки — свой интерпретатор того же представления. JIT работает только на arm64 macOS.
Эти цифры сняты в другом сеансе, не в том, что выше. Машина была сильно нагружена: load average от 83 до 165 на 8 ядрах. Поэтому абсолютные времена завышены. Каждое отношение ниже сравнивает варианты, которые гонялись вперемешку в одном сеансе, по 5 раз каждый.
| N | load avg | t27b test, мс (медиана / мин) | gen-c + clang + запуск, мс | дольше t27b (медиана / мин / CPU) | gen + zig test, мс | дольше t27b (медиана / мин / CPU) |
|---|---|---|---|---|---|---|
| 100 | 83–134 | 17,8 / 15,8 | 1158 | ×65,0 / ×59,0 / ×14,4 | 6798 | ×381,5 / ×349,2 / ×245,7 |
| 1000 | 129–161 | 59,6 / 51,1 | 1189 | ×19,9 / ×15,8 / ×8,6 | 6124 | ×102,7 / ×61,9 / ×67,4 |
| 5000 | 98–165 | 916,7 / 250,1 | 4529 | ×4,9 / ×8,3 / ×6,4 | 21 058 | ×23,0 / ×20,0 / ×24,9 |
Против пути через Zig t27b быстрее в 381 раза при N = 100 и в 23 раза при N = 5000 (медианы). По процессорному времени — в 246 и 25 раз. Медианное отношение для пути через C завышено нагрузкой. При N = 100 запуск ./ctest занял 970,3 мс по часам и всего 6,7 мс процессора — большую часть времени он ждал. Если считать только компиляцию, путь через C идёт в 10,5 раза дольше t27b при N = 100 и в 4,3 раза при N = 5000. Нагрузка видна и по zig test: при N = 5000 здесь он шёл 20,5 с, а в более спокойном прогоне выше — 3,4 с. Поэтому t27b нет в таблицах выше.
t27b build пишет .o, как clang -c. Тот же сеанс, медианы в миллисекундах:
| N | t27b build (trap) | gen-c + clang -O0 -c | gen-c + clang -O2 -c | C вручную, clang -O0 -c |
|---|---|---|---|---|
| 100 | 21,4 | 120,3 (×5,6) | 301,2 (×14,1) | 74,4 (×3,5) |
| 1000 | 62,9 | 570,5 (×9,1) | 1780,1 (×28,3) | 190,2 (×3,0) |
| 5000 | 822,7 | 3847,7 (×4,7) | 20 388,9 (×24,8) | 1496,0 (×1,8) |
Сравнение несимметричное: clang делает гораздо больше работы, чем t27b. Но место t27b оно показывает. При N = 5000 он быстрее, чем clang на C, написанном вручную, с -O0.
| Фаза, N = 5000, медиана 15 | мс | доля |
|---|---|---|
| разбор (общий с t27c) | 157,2 | 48,3% |
| проверка типов (общая с t27c) | 90,8 | 27,9% |
| перевод в своё IR | 37,8 | 11,6% |
| генерация кода A64 | 17,1 | 5,3% |
| отображение в память и запуск | 1,6 | 0,5% |
| всего, вместе с чтением файла | 325,8 | 100,0% |
76,1% времени уходит на фронтенд, общий у t27b с t27c. Собственная часть t27b, от перевода в IR до запуска, — 17,3%. Значит, ускорять t27b дальше — это ускорять фронтенд t27c.
| N | t27b trap | t27b wrap | gen-c + clang -O0 | gen-c + clang -O2 |
|---|---|---|---|---|
| 100 | 9,40 | 7,36 | 11,15 | 3,96 |
| 1000 | 10,36 | 8,47 | 12,46 | 4,93 |
| 5000 | 10,95 | 8,50 | 13,65 | 5,30 |
Наносекунды на вызов, медиана 5 прогонов. Контрольные суммы всех вариантов совпадают. Код t27b — примерно уровень clang -O0. В режиме trap он тратит 0,80–0,84 времени gen-c с -O0. В режиме wrap он в 1,60–1,86 раза медленнее gen-c с -O2. Оптимизатора в t27b нет. При N = 5000 секция кода — 400 000 байт в режиме trap и 319 992 в режиме wrap, против 963 612 и 523 608 у gen-c с clang при -O0 и -O2.
В дифференциальном тесте каждую функцию вызывали на одних и тех же входах из кода t27b и из кода clang. Режим wrap сравнивали с -O2, режим trap — с -O0 и ловушками UBSan на переполнение. На 12 261 000 вызовах расхождений 0. Это одна синтетическая программа, а не доказательство.
t27b corpus прошёл по всем 1174 спекам репозитория за 3,6 с. Понимает t27b 37 из них. 36 проходят, причём в 26 из них тестов нет вовсе; 1 падает из-за настоящей ошибки в спеке (ниже). Отказ — 1117 файлов, общий фронтенд споткнулся на 20. Расхождений JIT с интерпретатором, зависаний и падений не было. Чаще всего t27b отказывает (считается первый отказ в файле) на StructDecl (393), string literal (287), EnumDecl (89), InvariantBlock (70), type str (31). Структуры, строки, перечисления, блоки invariant и приведения — задача #5977, она открыта. Первый PR по покрытию, #5992, запускает блоки invariant как тесты и считает их отдельно; он отправлен, но не влит.
План дальше — запланировано и в работе, цифр пока нет: покрыть каждую спеку, которую проходит сам эталонный путь, двумя дорожками. Дорожка памяти добавляет структуры, массивы и срезы, строки — всё на одной модели «адрес плюс смещение», с данными только для чтения под константы. Скалярная дорожка добавляет перечисления, f64 и приведения. Порядок работы задаёт жадный рейтинг: следующей берётся та возможность, которая открывает больше всего ещё отвергнутых спек.
Чистая release-сборка самого t27b — 17 единиц сборки и 8 крейтов против 386 и 295 у t27c. Бинарник — 1,37 МБ против 15,6 МБ. Время его сборки при нагрузке 244–336 мало что значит: медиана 3 прогонов 111,6 с, минимум 67,2 с. Большая часть исходника, который он компилирует, не его: он подключает compiler.rs (44 636 строк) и use_resolve.rs (1010) рядом с примерно 5100 строками своего кода.
Почему он быстрый — не загадка. По той же причине Zig со своим бэкендом в 5,7 раза быстрее Zig через LLVM, и на том же стоят Copy-and-Patch и TPDE: один проход, без LLVM, код уровня -O0.
Второй бэкенд с определённой семантикой полезен уже тем, что его ответы можно сравнить с ответами t27c. Так нашлись четыре ошибки. Исправлена пока одна.
| Ошибка | Как нашли | Состояние на 4 октября 2026 |
|---|---|---|
ternary_model.t27: функция dot27 сдвигает i32 на величину до 52 бит, а её тест ожидал неверные значения | корпус t27b: «shift amount out of range at line 40» | исправлено: issue #5972 закрыт, PR #5975 влит 4 октября |
gen-c пишет обычный C (x + 1, x >> n). Знаковое переполнение и сдвиг шире типа в C — неопределённое поведение, и тесты inc_max и shr_big меняют исход между clang -O0 и -O2 | пробный модуль, три бэкенда рядом | в работе, ещё не PR |
t27c gen (Zig): знаковый % не компилируется, Zig требует @rem или @mod | тот же пробный модуль | отправлено, не влито: PR #5993 |
разборщик t27c читает module a.b; и use std.testing; как a плюс отдельное выражение .b без номера строки; так разобраны 12 спек | корпус t27b: отказ на «StmtExpr» в строке 0 | в работе, ещё не PR |
Главная из них — вторая. Одна и та же спека значит разное в зависимости от бэкенда и уровня оптимизации. Zig ловит переполнение, t27b тоже, а C, сгенерированный t27c, молча делает то, что решит оптимизатор. Исправление для Zig — PR #5993, отправлен, не влит. Исправления gen-c и разборщика лежат в локальных ветках и PR не стали, поэтому здесь они записаны как «в работе».
u32. Настоящие Rust и C++ теряют больше всего времени на обобщениях, шаблонах, макросах и импортах, а здесь их почти нет.clang -O2.Поработаем вместе
Работаю по контракту и part-time с hardware-AI, FPGA/RTL и ML-системами — от спецификации и открытого тулчейна до воспроизводимых измерений.