
Коротко о главномРазвернутьСвернуть
- Максим Шевалье-Буавер, автор языка Plush и в прошлом разработчица JIT-компилятора YJIT для Ruby, 2 сентября описала , как переписала виртуальную машину Plush со стекового байткода на регистровый.
- На наборе синтетических тестов новый интерпретатор быстрее старого в 2,07 раза по медиане и до 3,37 раза в лучшем случае; на тестах fib и binary_tree Plush обогнал Lua 5.
- Практический интерес здесь не в самом Plush, минималистичном динамическом языке на Rust, а в измеренном ответе на старый вопрос: сколько стоит стековая модель байткода.
Максим Шевалье-Буавер, автор языка Plush и в прошлом разработчица JIT-компилятора YJIT для Ruby, 2 сентября описала , как переписала виртуальную машину Plush со стекового байткода на регистровый. На наборе синтетических тестов новый интерпретатор быстрее старого в 2,07 раза по медиане и до 3,37 раза в лучшем случае; на тестах fib и binary_tree Plush обогнал Lua 5.5.1 на 24% и 55%.
Практический интерес здесь не в самом Plush, минималистичном динамическом языке на Rust, а в измеренном ответе на старый вопрос: сколько стоит стековая модель байткода. Вывод автора сформулирован жёстко: «вам, вероятно, не стоит писать стековые байткод-интерпретаторы в 2026 году» (перевод редакции). Для тех, кто пишет свои DSL, скриптовые движки или встраиваемые интерпретаторы, это готовый аргумент с цифрами и с честным списком того, что не ускорилось.
Почему стековая машина медленнее
В стековой 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


