Реклама
Перетяжка // Коробка 3.0

Интерпретатор Plush перешёл на регистры и ускорился до 3,37 раза

Замеры на MacBook Air M5 с rustc 1.96.0, по 11 чередующихся запусков на тест, медиана.

Обложка: Интерпретатор Plush перешёл на регистры и ускорился до 3,37 раза

Максим Шевалье-Буавер, автор языка Plush и в прошлом разработчица JIT-компилятора YJIT для Ruby, 2 сентября описала, как переписала виртуальную машину Plush со стекового байткода на регистровый. На наборе синтетических тестов новый интерпретатор быстрее старого в 2,07 раза по медиане и до 3,37 раза в лучшем случае; на тестах fib и binary_tree Plush обогнал Lua 5.5.1 на 24% и 55%.

Практический интерес здесь не в самом Plush, минималистичном динамическом языке на Rust, а в измеренном ответе на старый вопрос: сколько стоит стековая модель байткода. Вывод автора сформулирован жёстко: «вам, вероятно, не стоит писать стековые байткод-интерпретаторы в 2026 году» (перевод редакции). Для тех, кто пишет свои DSL, скриптовые движки или встраиваемые интерпретаторы, это готовый аргумент с цифрами и с честным списком того, что не ускорилось.

Ключевые выводы
  • Медианное ускорение 2,07 раза, максимальное 3,37; геометрическое среднее 1,94 раза, из них 1,55 дала сама регистровая модель без оптимизаций, ещё 25% добавили специализированные инструкции.
  • Инструкция теперь 64-битное слово вместо enum на 24 байта; трёхадресная форма reg(a) = reg(b) + reg(c), до 64 000 локальных переменных, 76 опкодов.
  • Замеры: MacBook Air M5 (10 ядер, arm64), rustc 1.96.0, 11 чередующихся запусков на тест, медиана; повторный прогон дал те же результаты.
  • Против Lua 5.5.1: быстрее на 24% в fib и на 55% в binary_tree; сравнение с CPython 3.14.6 и CRuby 4.0.6 автор ограничивает оговорками о разной семантике и составе тестов.
  • Тесты сборщика мусора почти не изменились: регистровая модель ускоряет диспетчеризацию, а не аллокацию.

Почему стековая машина медленнее

В стековой VM выражение a + b превращается в три инструкции: положить a, положить b, сложить. Каждая инструкция проходит через диспетчер: прочитать опкод, перейти на обработчик, сдвинуть указатель. В регистровой VM то же выражение записывается одной инструкцией с тремя операндами: куда положить и что сложить. Главный тезис статьи: в интерпретаторе самый большой рычаг производительности это число инструкций, которые нужно продиспетчеризовать, и регистровая модель сокращает его в разы. Автор цитирует это как основной вывод: «снова, в интерпретаторе главный рычаг это уменьшение числа инструкций» (перевод редакции).

Второй эффект: значения не гоняются между локальными переменными и временным стеком. Локальные переменные и есть регистры фрейма, поэтому x = y + z не требует ни загрузки, ни сохранения.

Как устроен новый байткод

Старая инструкция была Rust-перечислением Insn размером 24 байта. Новая упакована в 64-битное слово: опкод и операнды-регистры. Для сравнения автор приводит Lua: там 32-битное слово с 8-битными полями, отсюда лимит в 255 регистров на фрейм и 200 именованных локальных переменных. У Plush поля шире, до 64 000 локальных переменных, ценой вдвое большего размера инструкции. Всего опкодов 76. Сверху добавлены оптимизации, каждая из которых убирает инструкции: слитые сравнение-и-переход, сдвиги и маски с непосредственными операндами, свёртка констант, устранение лишних пересылок. Инлайн-кэши вынесены в отдельные таблицы, чтобы не раздувать слово инструкции.

Замену enum на 64-битное слово автор описывала неделей раньше, тогда это дало 17% на старой стековой модели; нынешняя статья седьмая в серии о Plush.

Что показали замеры и где их границы

Методика описана подробно: MacBook Air M5, rustc 1.96.0, для каждого сравнения 11 чередующихся запусков старой и новой версии, берётся медиана, весь набор прогнан дважды. Сравнивались конкретные коммиты репозитория. Неоптимизированная регистровая VM дала 1,55 раза по геометрическому среднему, оптимизации добавили ещё 25%, итого 1,94; медиана по отдельным тестам 2,07, максимум 3,37. Тесты, упирающиеся в сборщик мусора, остались на месте: диспетчеризация там не узкое место.

Сравнение с другими языками автор сопровождает оговорками: CPython 3.14.6, CRuby 4.0.6 и Lua 5.5.1 имеют другую семантику, а набор тестов синтетический. Заявлены только два прямых результата против Lua: fib на 24% быстрее, binary_tree на 55%. Отдельная демонстрация: программный рендеринг уровня Quake в 800 на 600 в один поток, около 47 кадров в секунду, с живой кучей до 1 ГБ без заметных пауз GC по оценке автора.

Что из этого забрать в свой интерпретатор

  • Если вы проектируете байткод с нуля, начинайте с регистровой модели: выигрыш от неё больше, чем от любой микрооптимизации диспетчера.
  • Считайте инструкции, а не такты: слитые инструкции (сравнение плюс переход) и непосредственные операнды окупаются именно сокращением диспетчеризации.
  • Не ждите ускорения там, где узкое место в аллокации: GC-тесты Plush не изменились.
  • Готовые бинарники Plush есть для Linux и macOS (x86-64 и arm64) и через PowerShell-установщик для Windows x86-64; код под Apache-2.0, примеры под CC0, всё в репозитории.

Кому это не пригодится: тем, кто использует готовые рантаймы и не пишет свои. CPython, к слову, остаётся стековой машиной, и разница подходов хорошо видна на его фоне; о цене операций в самом Python мы недавно писали в связи с новой 3.15.0rc2. Другой свежий пример переписанного интерпретатора с подробным разбором каждой оптимизации: Wasmi 2.0, где тоже сменили промежуточное представление и получили 2,2 раза.

Источники: Pointers Gone Wild: Plush's New Register-Based Interpreter, Репозиторий Plush, Предыдущая статья серии: Replacing a Rust enum with a 64-bit word