Производительность: воспроизводимый замер
Число без способа его повторить — не факт, а утверждение на веру. Поэтому
глава не приводит «скорость flang» вообще, а даёт харнесс
examples/spec/benchmark/, которым вы снимаете цифры на своей машине, и
показывает, какие цифры он дал на машине, где готовился курс, и при каких
условиях.
Минимальный пример
Харнесс меряет модель, которую строит сам. Вот она при трёх правилах — при замере правил 10, 30 и 100:
модуль «Замер»
объект «Заявка»
«объём»: неотрицательное
«премиум клиент»: признак
тотальная функция «Правило 1»
принимает заявка: «Заявка»
возвращает число
обеспечивает «правило 1 не больше единицы» результат не больше 1
если заявка.«объём» не меньше 1
то 1
иначе 0
тотальная функция «Правило 2»
принимает заявка: «Заявка»
возвращает число
обеспечивает «правило 2 не больше единицы» результат не больше 1
если заявка.«объём» не меньше 2
то 1
иначе 0
тотальная функция «Правило 3»
принимает заявка: «Заявка»
возвращает число
обеспечивает «правило 3 не больше единицы» результат не больше 1
если заявка.«объём» не меньше 3
то 1
иначе 0
тотальная функция «Сумма правил»
принимает заявка: «Заявка»
возвращает число
пример «все правила сработали»
дано заявка равно запись «Заявка» с «объём» равным 3 и «премиум клиент» равным нет
ожидается 3
(«Правило 1» от заявка) плюс («Правило 2» от заявка) плюс («Правило 3» от заявка)
Каждое правило несёт обещание, которое ядро доказывает, а на входе
«объём = число правил» срабатывают все правила — вызов проходит все ветви, а не
долю, которая зависела бы от данных. Текст модели детерминированный: функция
createSource в measure.mjs у вас построит ровно такой же.
Что делает компилятор
Харнесс меряет четыре операции:
| Операция | Команда | Что внутри |
|---|---|---|
check_proof |
flang check --proof |
разбор, типы, завершение, ядро доказательств, отчёт |
test |
flang test |
проверка и прогон примеров |
emit_js |
flang emit --target js |
проверка и печать модуля |
call |
вызов напечатанной функции | обычный JavaScript, без компилятора |
Первые три — работа компилятора: её платят в CI и при правке модели. Последняя — цена правила в рантайме вашего приложения, когда модель уже напечатана.
Считать может любой из двух компиляторов: двоичный flang (FLANG_BIN) или
сборка в WebAssembly, на которой работает песочница. Отчёт называет, какой из
двух мерили.
Методика замера
- Дорогие операции — медианой из пяти прогонов. Первый прогон холостой: у WebAssembly это компиляция модуля, у двоичного — прогрев кеша диска. У двоичного каждый прогон — новый процесс, и время старта входит в цифру: так его и зовут из CI.
- Вызов — микрозамером. Одиночный вызов идёт микросекунды, и на этом масштабе разрешение таймера сравнимо с измеряемым. Харнесс делает 5 холостых вызовов, подбирает батч так, чтобы замер шёл не меньше 2 мс, и делит время на размер батча.
- Медиана, а не среднее. Одна пауза сборщика мусора или соседний процесс,
укравший ядро, тянут среднее на себя, медиану — почти нет.
min/max/p95в отчёте показывают разброс. - Отказ — тоже результат. Если компилятор операцию не выполнил, строка отчёта называет код и причину, а не выдуманную цифру.
Ваши цифры за одну команду
node examples/spec/benchmark/measure.mjs
FLANG_BIN=~/.local/bin/flang node examples/spec/benchmark/measure.mjs
Вывод на машине, где готовился этот модуль: flang 0.7.29, node v26.7.0,
linux/x64, AMD EPYC 7742 64-Core Processor, 256 ядер. Машина общая, во
время замера на ней шли чужие задачи — средняя загрузка 34–37. Медиана в
миллисекундах, двоичный flang:
| Операция | 10 правил | 30 правил | 100 правил |
|---|---|---|---|
check_proof |
317.68 | 1317.02 | 6586.19 |
test |
122.67 | 445.21 | 1136.03 |
emit_js |
196.51 | 648.48 | 1870.43 |
call |
0.0007 | 0.0020 | 0.0066 |
То же в WebAssembly:
| Операция | 10 правил | 30 правил | 100 правил |
|---|---|---|---|
check_proof |
359.18 | 1437.29 | отказ |
test |
115.03 | 364.76 | 1071.64 |
emit_js |
205.04 | 584.24 | 1801.61 |
call |
0.0007 | 0.0022 | 0.0069 |
Полные отчёты с mean/min/max/p95 — в
examples/spec/benchmark/baseline-linux-x64-binary.json и
examples/spec/benchmark/baseline-linux-x64-wasm.json, снятых той же командой
с ключом --out.
Что видно из цифр:
- Дороже всего отчёт о доказательствах. При росте модели в 10 раз
check_proofдорожает в 20,7 раза, аtestиemit_js— в 9,3 и 9,5 раза. Ядро доказательств растёт быстрее размера модели; разбор, типы и примеры — примерно вровень с ним. - Вызов дешёвый и растёт линейно. 100 правил — 6,6 микросекунды на вызов, в 9,4 раза больше, чем 10. Напечатанная функция — обычный JavaScript, и цена правила в рантайме — это цена его ветвлений.
- У WebAssembly есть предел шагов. На 100 правилах
flang check --proofв WebAssembly останавливается:
FLANG_RECURSION_LIMIT: функция «Склеить до закрытия записи» исчерпала лимит шагов (2770894848) на глубине вызовов 0
Двоичный тот же файл проверяет за 6,6 секунды. test и emit_js в
WebAssembly на 100 правилах проходят: предел упирается в построение отчёта.
Отсюда практическое правило: большие модели проверяйте в CI двоичным, а
песочница годится для моделей размером с главу курса.
Сравнение с baseline
node examples/spec/benchmark/compare.mjs my.json examples/spec/benchmark/baseline-linux-x64-binary.json
compare.mjs печатает отклонение медианы в процентах операция за операцией.
Отчёты с другого железа или другого компилятора сравнивать буквально нельзя, и
инструмент об этом предупреждает. Вот сравнение двух baseline этой машины —
WebAssembly против двоичного:
текущий: flang 0.7.29 (WebAssembly), node v26.7.0, linux/x64, AMD EPYC 7742 64-Core Processor
baseline: flang 0.7.29 (двоичный), node v26.7.0, linux/x64, AMD EPYC 7742 64-Core Processor
ВНИМАНИЕ: отчёты сняты разными компиляторами (двоичный и WebAssembly) или разными версиями flang —
это сравнение двух инструментов, а не поиск регрессии.
operation scale current_ms baseline_ms diff_%
check_proof 10 359.1829 317.6813 +13.1%
test 10 115.0273 122.6653 -6.2%
emit_js 10 205.0385 196.511 +4.3%
call 10 0.0007 0.0007 0%
check_proof 30 1437.2911 1317.0195 +9.1%
test 30 364.7555 445.2098 -18.1%
emit_js 30 584.2429 648.4786 -9.9%
call 30 0.0022 0.002 +10%
test 100 1071.6421 1136.0297 -5.7%
emit_js 100 1801.605 1870.4332 -3.7%
call 100 0.0069 0.0066 +4.5%
check_proof@100: текущий — отказ, код 1, baseline — посчитано
На одной машине оба компилятора идут в пределах десятков процентов друг от
друга; разброс между ними того же порядка, что и шум загруженной машины.
Полезный сигнал — сравнить сегодняшний отчёт со вчерашним: тот же CPU, тот же
компилятор, тот же метод. Если check_proof на 100 правилах вырос втрое на
одинаковом железе — это регрессия модели или компилятора, а не шум.
Цена одного прогона без харнесса
Две команды flang отвечают на вопрос о цене без отдельного кода.
flang run --cost после значения печатает в поток ошибок число шагов
вычислителя, пик памяти и время:
flang run static/spec/models/order-discount.flang --trust --cost --function "Скидка по сумме" --args '{"сумма":20000,"постоянный":true}'
на веру: доказанность не считалась — запуск по ключу --trust
3000
цена прогона: шагов 118, пик памяти 19599360 байт, секунд 0.086407
Шаги — свойство программы, секунды — свойство машины. flang --machine снимает
постоянную машины: сколько шагов разбора и лексера она делает в секунду, и
заодно называет загрузку:
машина
ядер: 256
занято ядер: 84.45
свободно ядер: 171.55
витков в секунду, разбор: 257340938
витков в секунду, лексер: 542350029
разброс: 0.47
Ключ --step-limit N поднимает предел шагов самого компилятора на один прогон —
им проверяют большую модель, которая упёрлась в умолчание. --max-steps N у
flang run ограничивает вычисление функции, а у flang emit впечатывается в
напечатанную программу; своему прогону он не говорит ничего.
Что эти цифры не означают
Харнесс меряет компилятор flang и напечатанную функцию в Node.js. Он не меряет:
- сборку бандлером вокруг напечатанного кода и
tscпо всему проекту; - сеть и диск;
- поведение под конкурентной нагрузкой — харнесс однопоточный и последовательный.
Порядок величин стоит назвать. Проверка модели размером с главу курса — десятки полей и правил — укладывается в десятые доли секунды или в пару секунд. Это цена шага CI и правки модели, а не запроса: в рантайм компилятор не едет вовсе, там работает напечатанная функция за микросекунды. Явно мерить стоит, когда правил сотни: отчёт о доказательствах растёт быстрее модели, и на сотне правил он уже идёт секунды.
Замер в CI
Регрессия ловится не абсолютным порогом — шумные раннеры на общем CPU дают разброс, из-за которого фиксированный процент либо ложно срабатывает, либо ничего не ловит, — а сравнением с прошлым отчётом на том же раннере:
- run: node examples/spec/benchmark/measure.mjs --out current.json
- run: node examples/spec/benchmark/compare.mjs current.json baseline.json
# baseline.json — артефакт предыдущего успешного прогона на master,
# сохранённый через actions/cache или actions/upload-artifact
- run: node examples/spec/benchmark/compare.mjs current.json baseline.json --json > diff.json
Дальше — небольшой скрипт-обвязка (в харнесс не входит), который читает
diff.json и падает, если diff_percent на нужной операции и масштабе
превысил порог с запасом на шум раннера — например, вдвое, а не на 10 %. Смысл
в том, чтобы ловить скачки на порядок, а не дёргать команду на каждый процент.
Строки с отказом compare.mjs печатает отдельно: если операция раньше
считалась, а теперь упирается в предел, это сигнал важнее любого процента.
Практика в песочнице
Песочница тоже показывает время: под ответом стоит «N ms» — сколько шла
проверка в вашем браузере. Это не то же измерение, что делает measure.mjs: в
песочнице это один прогон без прогрева и без медианы, а самую первую проверку
ещё и замедляет загрузка компилятора — около 9,3 МБ сжатыми, дальше он берётся
из кеша браузера. Такое число полезно как ощущение «на этом устройстве это
быстро», но не годится для сравнения между запусками. Харнесс нужен для
другого — для тренда, который можно класть в CI и сравнивать день за днём.
Чек-лист
- Снимайте цифры
measure.mjsна своей машине — не переносите чужие как свои. - Называйте компилятор: двоичный и WebAssembly — разные инструменты.
- Сравнивайте
compare.mjsв первую очередь с прошлым отчётом на том же железе; чужой baseline — только по форме роста. - Большие модели проверяйте в CI двоичным: у WebAssembly предел шагов ниже.
- Помните, что дорогая часть — отчёт о доказательствах в CI, а не вызов напечатанной функции в рантайме.
- Для CI сравнивайте с прошлым прогоном на том же раннере и берите порог с запасом на шум.