Исполняемые спецификации на flang Производительность: воспроизводимый замер
0%

Производительность: воспроизводимый замер

Производительность: воспроизводимый замер

Число без способа его повторить — не факт, а утверждение на веру. Поэтому глава не приводит «скорость 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 сравнивайте с прошлым прогоном на том же раннере и берите порог с запасом на шум.

Кейсы каталога по этой теме

Дальше: сквозной проект

Нашли неточность? Выделите фрагмент текста — рядом появится жучок.

Нужен разбор именно вашей ситуации?

Статья описывает общий случай. Если у вас частный — можно разобрать его отдельно, платно. А если не хватает целого материала, предложите тему: её оплачивают вскладчину, и она выходит открытой для всех.

Доска запросов
Дальше