T27.AI

Блог

Компилятор в 3 МБ, который превращает спеку в код AArch64 за 5 мс, и где он всё ещё проигрывает

2026-10-11 · 12 мин чтения

[замер на общей Railway-лаборатории; время выполнения — эмуляция qemu, счёт инструкций точный; код t27b уровня -O0] t27b 0.2.0 вышел с подписанной квитанцией на тот самый бинарник, что лежит в релизе. Как нативный кросс-компилятор на x86_64 он компилирует спеку t27 вместе с тестами примерно за 4,8 мс — в 8,8 раза быстрее, чем gen-c и gcc -O0, и примерно в 640 раз быстрее zig Debug на спеку, — занимая 5,3 МиБ памяти, одним бинарником примерно в 3 МБ, и сходится с эталоном на всех 8344 тестах, которые прогнали обе стороны. Прорывом в сгенерированном коде это не назовёшь: он выполняет в 3,4 раза больше инструкций, чем gcc -O2, и в 4,5 раза больше, чем zig ReleaseFast, его код до 2 раз больше, а на Linux это JIT для тестов, который пока не умеет писать ELF. За неделю его доля среди спек, которые проходит эталон, выросла с 8,6% до 98,9%.

Гравюрный триптих к статье: Компилятор в 3 МБ, который превращает спеку в код AArch64 за 5 мс, и где он всё ещё проигрывает
Три панели, слева направо

Подписи на изображении — на английском.

  1. ONE BINARYThree megabytes, no linker.
  2. MILLISECONDSA spec compiled in about 5 ms.
  3. THE LIMITExact on every test, -O0 in speed.
Открыть полный триптих в исходном размере
#t27#Compiler#Measurement#Verification

Вот компилятор, который скачивается одним файлом примерно в 3 МБ. Дайте ему спеку .t27 — и примерно через 5 миллисекунд он её разобрал, проверил типы, понизил и записал машинный код AArch64 для каждой функции и каждого теста в ней. Ему не нужны ни LLVM, ни ассемблер, ни линкер, а за работой он занимает около 5 МиБ памяти. Это t27b, нативный бэкенд языка t27. 10 октября он вышел версией 0.2.0 вместе с подписанной квитанцией на тот самый бинарник, что лежит в релизе.

Мы сравнили его с zig, gcc, clang и rustc на одних и тех же спеках и одной машине, и честный результат состоит из двух половин. t27b очень быстрый, очень маленький и сходится с эталоном на каждом тесте. Код, который он пишет, медленный: примерно уровня gcc -O0 и в три–пять раз хуже любого оптимизирующего компилятора. В этом посте — обе половины, как их мерили и как проверить их самому.

t27b 0.2.0 · check the binary, compile a spec

Записано на Railway-лаборатории t27c по ssh 10 октября в 23:38 UTC, в клоне t27 на теге t27b-v0.2.0. Бинарник релиза и его квитанция скачиваются с GitHub; sha256 бинарника равен полю t27b_sha256 квитанции; t27c corpus-receipt compare печатает EQUIVALENT, код выхода 0; нативный x86_64 t27b, собранный на теге, за несколько миллисекунд пишет объектник Mach-O для AArch64 из kernel_matmul (строка real у time); затем бинарник релиза прогоняет два теста спеки в своём JIT под qemu-aarch64, где каждая фаза эмулируется и идёт медленнее. 34 с; приглашение и набор команд поставлены, вывод — то, что напечатала лаборатория. Открыть запись на отдельной странице.

Что такое t27b

t27 — язык, где сначала пишется спека: файл .t27 содержит типы, функции, тесты и инварианты, а t27c превращает его в Zig, C, Rust или Verilog. t27b — другая дорога. Он читает тот же файл через подключённый фронтенд t27c, понижает его в собственное IR и сразу выдаёт машинный код AArch64. t27b test прогоняет каждый тест во встроенном JIT и сверяет результат с собственным интерпретатором IR; t27b build пишет объектник Mach-O. Новые решения о понижении живут в плановых спеках .t27, которые t27c gen-rust превращает в Rust: по правилу проекта новая логика пишется на t27.

Результат одной таблицей

Осьt27bОстальныеДля t27b
Время компиляции спеки4,8 мс, нативный кросс-компилятор на x86_64, с тестамиgen-c + gcc -O0 42 мс; gen + zig Debug 3,05 с; gen + zig ReleaseFast 13,8 свыигрыш: 8,8x, 640x, 2900x
Все 20 спек, zig одним вызовом0,72 сzig Debug 6,21 с; zig ReleaseFast 32,9 свыигрыш меньше: 8,6x и 46x
Пиковая память при компиляции5,3 МиБ (медиана по спекам)zig 354–500 МиБ; gcc 27–32; clang 68–76; rustc 112–120выигрыш
Что нужно установитьодин бинарник примерно в 3 МБzig 363 МБ; gcc для aarch64 около 145 МБ; rustc 790 МБвыигрыш, но слинковать исполняемый файл он не может
Совпадение с эталономрасходятся 0 из 8344 тестовzig ReleaseFast меняет вердикт 7 файлов; gen-c + clang расходится сам с собой на 39точно
Пересборкабит в бит, и из другого каталога тожеgcc (без -g) и rustc тоже; zig записывает каталог сборкинаравне с лучшими
Сколько инструкций выполняет его кодпримерно как gcc -O0 (0,91x)у gcc -O2 в 3,4 раза меньше; у zig ReleaseFast в 4,5 разапроигрыш
Размер кодав 1,8–2,0 раза больше оптимизирующих сборокgcc -O2, zig ReleaseSafe, zig ReleaseFastпроигрыш
Покрытие на коммите замера1296 из 1311 проходов эталона (98,9%)zig под aarch64: 1311чуть позади

Время компиляции: миллисекунды против секунд

Лаборатория — машина x86_64, поэтому t27b собрали и под неё. На x86_64 t27b test проходит фронтенд и генератор кода вместе с тестами и останавливается, только когда пытается отобразить JIT; время работы этого процесса и есть время компиляции t27b. Все остальные конвейеры тоже шли нативно, со временем генератора t27c в своём счёте. Один процесс за раз, шесть кругов по спекам и конвейерам по очереди, в отчёте тёплые медианы.

Время компиляции спеки вместе с тестами (лог. шкала)среднее геометрическое тёплых медиан по 18 спекам, нативно на x86_64, с генератором1 мс10 мс100 мс1 с10 сt27b: спека + тестыt27b: спека + тесты: 4,8 мс4,8 мсt27b build: объектникt27b build: объектник: 5,1 мс5,1 мсgen-c + gcc -O0gen-c + gcc -O0: 42,0 мс · 8,8x42,0 мс · 8,8xgen-c + clang -O0gen-c + clang -O0: 71,1 мс · 15x71,1 мс · 15xgen-c + gcc -O2gen-c + gcc -O2: 100 мс · 21x100 мс · 21xgen-c + clang -O2gen-c + clang -O2: 105 мс · 22x105 мс · 22xgen + zig Debuggen + zig Debug: 3,05 с · 642x3,05 с · 642xgen + zig ReleaseFastgen + zig ReleaseFast: 13,8 с · 2 906x13,8 с · 2 906xВсе 20 спек, суммарное время (лог. шкала)только компиляторы; zig делит постоянные затраты на один вызов, у t27b такого режима нет1 с10 с100 с1000 сt27b, 20 процессовt27b, 20 процессов: 0,72 с0,72 сgcc -O0, 20 процессовgcc -O0, 20 процессов: 1,31 с · 1,8x1,31 с · 1,8xzig Debug, один вызовzig Debug, один вызов: 6,21 с · 8,6x6,21 с · 8,6xzig ReleaseFast, один вызовzig ReleaseFast, один вызов: 32,9 с · 46x32,9 с · 46xzig Debug, 20 вызововzig Debug, 20 вызовов: 61,7 с · 86x61,7 с · 86xzig ReleaseFast, 20 вызововzig ReleaseFast, 20 вызовов: 284 с · 395x284 с · 395x
Сверху: среднее геометрическое тёплых медиан по 18 спекам первого уровня, которые собрал каждый шаг. Снизу: сумма по всем 20 спекам первого уровня, только процессы компиляторов, как в таблице отчёта (генератор t27c добавляет к строкам gcc и zig около 0,2 с на 20 спек). Нативно на x86_64, общая Railway-лаборатория t27c, 10 октября; нагрузка машины от 27 до 103 на 48 потоках. Источник: t27b-bench-2026-10.json, ось B.

Большая часть времени zig на спеку — постоянные затраты: сборка запускалки тестов, частей std для паники и форматирования и линковка, всё через LLVM. Маленькие и большие спеки одинаково заняли 2,7–3,4 с в Debug. Если собрать все 20 спек одним вызовом, zig тратит 0,31 с на спеку: разрыв падает примерно с 640x до 8,6x в Debug и примерно с 2900x до 46x в ReleaseFast. Многофайлового режима у t27b нет, поэтому его сумма — 20 отдельных процессов. На 567 файлах, которые проходят все конвейеры, t27b компилирует 183 файла в секунду; gen-c и gcc -O0 — 28.

Одна поправка, которую мы должны: в прежних отчётах время компиляции t27b называлось 33–86 мс. Его мерили под qemu. Сверка с нативной сборкой на тех же спеках показала, что эмуляция замедляла фазы компиляции t27b примерно в 20 раз (среднее геометрическое), так что нативная цифра на порядок меньше.

Память и размер

Пиковая память при компиляции (МиБ, лог. шкала)медиана по спекам оси C; всё дерево процессов, по wait41101001000t27bt27b: 5,3 МиБ5,3 МиБgcc -O0gcc -O0: 27,1 МиБ · 5,2x27,1 МиБ · 5,2xgcc -O2gcc -O2: 31,6 МиБ · 6,0x31,6 МиБ · 6,0xclang -O0clang -O0: 68,0 МиБ · 13x68,0 МиБ · 13xclang -O2clang -O2: 75,5 МиБ · 14x75,5 МиБ · 14xrustc -O0rustc -O0: 112 МиБ · 21x112 МиБ · 21xzig Debugzig Debug: 354 МиБ · 67x354 МиБ · 67xzig ReleaseFastzig ReleaseFast: 500 МиБ · 95x500 МиБ · 95xЧто нужно установить (МБ, лог. шкала)t27b не нужны ассемблер и линкер; зато и собрать исполняемый файл он не может1101001000t27b, один бинарникt27b, один бинарник: 3,1 МБ3,1 МБgcc 12 для aarch64 (около)gcc 12 для aarch64 (около): 145 МБ · 47x145 МБ · 47xzig 0.16 (с clang)zig 0.16 (с clang): 363 МБ · 118x363 МБ · 118xтулчейн rustc 1.99тулчейн rustc 1.99: 790 МБ · 257x790 МБ · 257x
Сверху: пиковая резидентная память всего дерева процессов каждого компилятора (у gcc вместе с cc1, as и ld), прочитанная из wait4 маленьким статическим помощником; медиана по спекам оси C. Снизу: что нужно установить. Размер t27b — его сборка под x86_64 на коммите замера; бинарник для aarch64 в релизе весит 3,09 МБ. Источник: t27b-bench-2026-10.json, ось D.

Медиана t27b — 5,3 МиБ, самой прожорливой спеке понадобилось 48 МиБ. zig нужны 354 МиБ в Debug и 500 МиБ в ReleaseFast, примерно в 67 и 95 раз больше. Установка говорит то же самое: один исполняемый файл со статически вшитой std Rust, которому во время работы нужна glibc, против сотен мегабайт компилятора, исходников libc и LLVM. У этой малости есть настоящая цена: слинковать исполняемый файл t27b не может.

Точно по эталону

Эталон — t27c gen, затем zig test в Debug, нативно. t27b прогнал весь корпус, 1819 файлов, через свой JIT и свой интерпретатор, и везде, где обе стороны запускали тесты файла, они сравнивались тест за тестом: расходятся 0 из 8344 тестов. zig Debug, собранный под aarch64 и запущенный под тем же qemu, тоже сходится с t27b на всех 8344.

Оптимизаторы менее точны. zig ReleaseFast меняет вердикт 7 файлов по сравнению с zig Debug; в одном из них тест читает ячейку стека после того, как её кадр уже вернулся. На дороге через C хуже. В выводе gen-c есть неопределённое поведение, поэтому вердикт зависит от уровня оптимизации: clang -O0 и -O2 расходятся на 39 файлах и 107 тестах. Самый маленький пример — функция, которую gen-c печатает как bool dummy(void) { true; }, без return. Она проходит под gcc -O0 и clang -O2 и падает под gcc -O2 и clang -O0.

Каждый конвейер детерминирован на месте. t27b, генераторы t27c, gcc без -g и rustc выдают бит в бит то же самое и при сборке из другого каталога; zig и zig cc записывают каталог сборки в отладочную информацию. На этом свойстве держатся подписанные квитанции лаборатории t27b: два прогона одного коммита должны дать один корень Меркла.

Где t27b проигрывает

Код t27b, делённый на их код (ср. геом., 20 спек)больше 1 — у t27b больше инструкций или байт; счёт инструкций не зависит от машинывыполнено инструкцийразмер кода0x1x2x3x4x5x← t27b лучшеt27b хуже →zig Debugzig Debug, выполнено инструкций: 0,54x0,54xzig Debug, размер кода: 0,66x0,66xclang -O0clang -O0, выполнено инструкций: 0,41x0,41xclang -O0, размер кода: 0,65x0,65xgcc -O0gcc -O0, выполнено инструкций: 0,91x0,91xgcc -O0, размер кода: 1,34x1,34xzig ReleaseSafezig ReleaseSafe, выполнено инструкций: 3,17x3,17xzig ReleaseSafe, размер кода: 1,89x1,89xgcc -O2gcc -O2, выполнено инструкций: 3,43x3,43xgcc -O2, размер кода: 1,82x1,82xclang -O2 (9 спек)clang -O2 (9 спек), выполнено инструкций: 3,50x3,50xclang -O2 (9 спек), размер кода: 1,69x1,69xzig ReleaseFastzig ReleaseFast, выполнено инструкций: 4,47x4,47xzig ReleaseFast, размер кода: 2,03x2,03x
Код t27b, делённый на код каждого конвейера: среднее геометрическое по 20 спекам первого уровня (у clang -O2 по 9, где он не вычислил тест при компиляции). Инструкции считал плагин TCG для qemu, от машины счёт не зависит. Спеки выбирались, не глядя на t27b: 20 с наибольшим числом инструкций под gcc -O0 среди 567 файлов, которые проходят все конвейеры. Источник: t27b-bench-2026-10.json, ось C.

Это та половина результата, из которой не выйдет заголовка. Код t27b выполняет примерно столько же инструкций, сколько gcc -O0, и примерно вдвое меньше, чем zig Debug. Но любой оптимизирующей сборке он уступает в 3–5 раз: в 3,2 раза zig ReleaseSafe, сборке с теми же проверками во время выполнения; в 3,4 раза gcc -O2; в 4,5 раза zig ReleaseFast. Под qemu это в 3,4–4,1 раза больше времени, а на криптографических спеках разрыв с zig ReleaseFast доходит до 10 раз по инструкциям.

ГдеЧислоЧто это значит
Сгенерированный кодв 3,4 раза больше инструкций, чем у gcc -O2; в 3,2 раза, чем у zig ReleaseSafe; в 4,5 раза, чем у zig ReleaseFastпрограмма, собранная t27b, работает медленнее той же программы от любого оптимизатора
Размер кодав 1,8–2,0 раза больше любой оптимизирующей сборкибольше байт на те же функции
Покрытие1296 из 1311 на коммите замера (файлы с тестами); 1349 из 1364 в релизе (подсчёт лаборатории)всё ещё блокируют i128 этапа 3, @cos и два пустых типизированных литерала массива
Исполняемые файлы для Linuxтолько объектники Mach-O: ни ELF, ни линковкина aarch64-linux t27b гоняет тесты в JIT, выпустить бинарник он не может
Самогенерация36% бэкенда сгенерированы из .t27; 12% с учётом подключённого фронтендабольшая часть кода, который исполняет t27b, — всё ещё Rust, написанный руками
DDCDDC_BACKEND_AGREEMENT, а не DDC_INDEPENDENTу трёх путей один общий фронтенд
Случайные программы7 расхождений на 1000 программ (3 в прогоне релиза)фаззер достаёт код, которого нет в корпусе, и всё ещё находит различия
Сброс перед тестомt27core идёт в 8,3 раза медленнее, чем под zig Debugjit.call копирует всё состояние модуля перед каждым тестом
Амортизацияотрыв в 640x падает до 8,6x, когда zig собирает 20 спек разоммногофайлового режима у t27b нет

Две цены поменьше. Проверки переполнения, включённые по умолчанию, стоят около 12% инструкций и 24% эмулированного времени. А одна спека, compiler/core/t27core, идёт в 8,3 раза медленнее, чем под zig Debug, хотя t27b выполняет меньше инструкций: перед каждым тестом jit.call копирует всё состояние модуля, чтобы каждый тест начинался с чистого листа, и каждый из 95 тестов t27core платит за эту копию не меньше 2,8 мс.

Как мерили и чему не доверять

Неделя покрытия

Доля проходов эталона, которые проходит t27b (4–10 окт)каждый прогон master в лаборатории t27b; знаменатель вырос с 648 до 1364 вместе с корпусом и эталономпроходит с проверкамипроходит пусто (ни один assert не выполнился)0%25%50%75%100%4 окт5 окт6 окт7 окт8 окт9 окт10 окт2026-10-04 16:06Z dd3864dff: 56 из 648 (8,6%)2026-10-04 22:13Z aa57ba405: 190 из 719 (26,4%)2026-10-05 05:23Z df7b627b6: 370 из 720 (51,4%)2026-10-05 12:51Z 03362c1da: 379 из 722 (52,5%)2026-10-05 19:05Z 1f9b3706a: 428 из 773 (55,4%), +283 пустых2026-10-06 01:49Z 099ac2224: 457 из 791 (57,8%), +261 пустых2026-10-06 09:47Z 0241b52f9: 462 из 795 (58,1%), +261 пустых2026-10-06 15:56Z d4a4f82cc: 519 из 828 (62,7%), +247 пустых2026-10-06 22:18Z b1d823579: 541 из 844 (64,1%), +247 пустых2026-10-07 04:43Z 9c6d32617: 559 из 855 (65,4%), +245 пустых2026-10-07 10:53Z 68fb92807: 827 из 1 045 (79,1%), +151 пустых2026-10-07 17:10Z 76a9de9ab: 853 из 1 074 (79,4%), +150 пустых2026-10-07 23:20Z 4dd14bf69: 875 из 1 084 (80,7%), +150 пустых2026-10-08 05:50Z 2e06cef59: 899 из 1 099 (81,8%), +150 пустых2026-10-08 12:13Z 579bc55aa: 924 из 1 115 (82,9%), +151 пустых2026-10-08 18:15Z 7c884488c: 988 из 1 177 (83,9%), +152 пустых2026-10-09 00:18Z a40d89128: 1 021 из 1 193 (85,6%), +142 пустых2026-10-09 06:19Z f97e761a4: 1 039 из 1 211 (85,8%), +142 пустых2026-10-09 12:38Z 30c9755c7: 1 094 из 1 219 (89,7%), +100 пустых2026-10-09 19:12Z b9d22d0f4: 1 173 из 1 249 (93,9%), +56 пустых2026-10-10 02:21Z b0c63a215: 1 201 из 1 279 (93,9%), +56 пустых2026-10-10 08:40Z 7c16bfab0: 1 208 из 1 281 (94,3%), +50 пустых2026-10-10 15:18Z 26a975354: 1 277 из 1 309 (97,6%), +5 пустых2026-10-10 21:43Z a977fd715: 1 339 из 1 354 (98,9%), +5 пустых2026-10-10 22:34Z 59e2c87c4: 1 349 из 1 364 (98,9%), +5 пустых56 из 648 (8,6%)v0.1.0: 899 из 1 099 (81,8%)v0.2.0: 1 349 из 1 364 (98,9%)
Каждая точка — один прогон master в лаборатории t27b, с 4 по 10 октября: файлы, которые t27b проходит с проверками во время выполнения, делённые на файлы, которые проходит эталон. Полоса над линией — пустые проходы (все тесты прошли, ни один assert не выполнился), их считают по файлам с 5 октября 15:43 UTC. Знаменатель вырос с 648 до 1364 вместе с корпусом и эталоном. Источник: сводки прогонов на t27b-lab-production.up.railway.app/runs/.

4 октября t27b проходил 56 из 648 спек, которые проходил эталон. v0.1.0 8 октября проходила 899 из 1099 с проверками и ещё 150 пусто. v0.2.0 проходит 1349 из 1364, пустых 5, и ни один из 8629 сравнённых тестов не расходится с эталоном. Между коммитом замера и релизом пали ещё четыре блокера, и проходы t27b выросли с 1312 до 1349 при росте проходов эталона с 1331 до 1364: мономорфизация anytype (t27#8738), i128 во время выполнения, этапы 1 и 2 (t27#8750, t27#8795), и кадры до 16 МиБ локальных агрегатов (t27#8764).

Что осталось в журнале: i128 во время выполнения, этап 3, для tri/t27b/eval_arith.t27, @cos (t27#8677), два пустых типизированных литерала массива, пять заглушек, которые проходят, ничего не проверяя (t27#8766), и пять строк, где эталон проходит по неверной причине, а t27b прав, что отказывается.

Почему это можно проверить

Бинарник в релизе — не пересборка. Это тот самый файл, которым лаборатория t27b прогнала весь корпус, и его sha256, 0202a2ae..., — это поле t27b_sha256 в квитанции лаборатории на этот прогон. Квитанция подписана Ed25519 под строкой домена t27-corpus-receipt-v1 и несёт корни Меркла над каждым входным файлом, каждым вердиктом и каждым сгенерированным листингом. Из клона t27 команда t27c corpus-receipt compare R R проверяет подпись и заново хеширует каждый лист:

base "59e2c87c4bd74c1137f4071b5e63095a285ce5e4" AUTH_MISSING_NONE AUTHOR leaves-bound true
head "59e2c87c4bd74c1137f4071b5e63095a285ce5e4" AUTH_MISSING_NONE AUTHOR leaves-bound true
totals true inputs true verdicts true outputs true: EQUIVALENT

Каждый прогон корпуса ещё и выполняет каждый тест дважды, в JIT и в собственном интерпретаторе IR, и расхождений 0. А подписанная квитанция DDC собирает самокомпиляцию t27core тремя путями — через gen-c и cc, через t27b и через zig, — и все три дайджеста второй ступени совпадают.

Теперь пределы, потому что в них весь смысл упражнения. Вердикт DDC — DDC_BACKEND_AGREEMENT, а не DDC_INDEPENDENT: у всех трёх путей общий фронтенд t27c. JIT и интерпретатор читают одно и то же IR. Эталон, с которым t27b сходится на 8344 тестах, выходит из того же фронтенда, который подключает t27b. Фаззер случайных программ — единственная проверка, которая выходит за пределы корпуса, и он всё ещё находит расхождения: 7 на 1000 программ в прогоне, который цитирует отчёт, и 3 в прогоне релиза. Ни одна проверка здесь не сравнивает t27b с независимо написанным фронтендом t27. Проверка необычно наглядна, но по большей части это всё ещё согласованность с самим собой.

Что дальше

Вердикт

Не прорыв в сгенерированном коде. Быстрый, лёгкий базовый компилятор, точный по своему эталону: компилятор в 3 МБ без LLVM, которому нужно около 5 МиБ памяти, который превращает спеку в код AArch64 примерно за 5 мс и сходится с эталоном на каждом из 8344 тестов. Это та ось, с которой стоит начинать, а разрыв в 3–5 раз в качестве кода — та, над которой стоит работать. Релиз, отчёт о замере со всеми оговорками (t27#8855), цикл улучшений (эпик t27#8326) и квитанция прогона релиза — в ссылках ниже.

Чего это не решает

Пруфы

Поработаем вместе

Хотите так же проверить собственный дизайн?

Я аудирую RTL и строю независимые побитово точные модели, затем провожу результат через синтез и, когда это полезно, проверяю на плате Artix-7. Первый модуль проверки — бесплатно.