Блог
[замер на общей 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 МБ. Дайте ему спеку .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
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 в своём счёте. Один процесс за раз, шесть кругов по спекам и конвейерам по очереди, в отчёте тёплые медианы.
Большая часть времени 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 раз (среднее геометрическое), так что нативная цифра на порядок меньше.
Медиана 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 выполняет примерно столько же инструкций, сколько 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, написанный руками |
| DDC | DDC_BACKEND_AGREEMENT, а не DDC_INDEPENDENT | у трёх путей один общий фронтенд |
| Случайные программы | 7 расхождений на 1000 программ (3 в прогоне релиза) | фаззер достаёт код, которого нет в корпусе, и всё ещё находит различия |
| Сброс перед тестом | t27core идёт в 8,3 раза медленнее, чем под zig Debug | jit.call копирует всё состояние модуля перед каждым тестом |
| Амортизация | отрыв в 640x падает до 8,6x, когда zig собирает 20 спек разом | многофайлового режима у t27b нет |
Две цены поменьше. Проверки переполнения, включённые по умолчанию, стоят около 12% инструкций и 24% эмулированного времени. А одна спека, compiler/core/t27core, идёт в 8,3 раза медленнее, чем под zig Debug, хотя t27b выполняет меньше инструкций: перед каждым тестом jit.call копирует всё состояние модуля, чтобы каждый тест начинался с чистого листа, и каждый из 95 тестов t27core платит за эту копию не меньше 2,8 мс.
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. Проверка необычно наглядна, но по большей части это всё ещё согласованность с самим собой.
@cos — последние нереализованные блокеры в журнале..t27 сгенерированы 36% бэкенда t27b и 12% кода, который он исполняет; правило владельца — ни одно изменение не может увеличить написанный руками Rust в cli/t27b.Не прорыв в сгенерированном коде. Быстрый, лёгкий базовый компилятор, точный по своему эталону: компилятор в 3 МБ без LLVM, которому нужно около 5 МиБ памяти, который превращает спеку в код AArch64 примерно за 5 мс и сходится с эталоном на каждом из 8344 тестов. Это та ось, с которой стоит начинать, а разрыв в 3–5 раз в качестве кода — та, над которой стоит работать. Релиз, отчёт о замере со всеми оговорками (t27#8855), цикл улучшений (эпик t27#8326) и квитанция прогона релиза — в ссылках ниже.
Поработаем вместе
Я аудирую RTL и строю независимые побитово точные модели, затем провожу результат через синтез и, когда это полезно, проверяю на плате Artix-7. Первый модуль проверки — бесплатно.