Компиляторы
Байткод и виртуальные машины
JVM запускает один и тот же .class файл на Windows, Linux, macOS, x86-64, ARM64 - без перекомпиляции. Python-скрипт работает везде где есть интерпретатор. Байткод - абстракция над железом: один раз скомпилировать в portable bytecode, потом запускать везде. Именно это сделало Java 'write once, run anywhere'.
- **CPython 3.11** ускорился на 25% благодаря специализированным bytecode инструкциям (Faster CPython). Вместо одного BINARY_ADD - отдельные BINARY_OP_ADD_INT, BINARY_OP_ADD_FLOAT. Адаптивная специализация на основе наблюдаемых типов.
- **WebAssembly** - новый универсальный bytecode для web и beyond. Wasmtime, wasmer - standalone WASM runtimes. WASI (WebAssembly System Interface) позволяет WASM работать вне браузера как безопасный sandbox.
- **Lua 5** выбрал register-based VM и обогнал CPython в ~3x на benchmark по числодробилкам. LuaJIT (Mike Pall) добавил JIT компиляцию и достиг производительности, сопоставимой с C, для числовых задач.
Stack Machine
Stack machine - виртуальная машина, использующая стек для хранения промежуточных значений. Инструкции работают неявно с вершиной стека: `IADD` снимает два верхних значения, складывает, кладёт результат. Не нужно указывать операнды явно.
WebAssembly - stack machine с явным стеком и структурным control flow (нет goto). Компактный бинарный формат: `0x6a` = i32.add, `0x20 0x00` = local.get $0. Среднее WASM: 10-15 байт на одну инструкцию-концепт против 25+ для x86. Stack machine проще для верификации: тип стека отслеживается при валидации, неправильные программы отвергаются до выполнения.
Каков порядок операндов при выполнении `ISUB` на stack machine если стек `[5, 3]` (3 на вершине)?
Register-Based VM
Register-based VM использует явные виртуальные регистры вместо стека. Инструкции явно указывают источники и назначение: `ADD R1, R2, R3`. Меньше инструкций для того же вычисления, но каждая инструкция шире (3 операнда вместо 0).
CPython перешёл на специализированный bytecode в 3.11 (Faster CPython project): вместо BINARY_ADD появились BINARY_OP_ADD_INT, BINARY_OP_ADD_FLOAT - специализированные версии для конкретных типов. Это adaptive interpretation: первые несколько исполнений определяют типы, потом заменяют generic инструкцию на специализированную. CPython 3.11 быстрее 3.10 на 25% именно благодаря этому.
Почему Lua и Dalvik выбрали register-based VM вместо stack-based?
Bytecode Design
Дизайн байткода влияет на производительность интерпретатора, размер файлов, удобство верификации и JIT-компиляции. Ключевые решения: ширина инструкции (фиксированная vs переменная), число opcodes, линейность vs структурированность.
WASM использует LEB128 (Little Endian Base 128) для кодирования целых чисел переменной длины: малые числа (0-127) кодируются в 1 байт, большие - в 2-5 байт. Это почти всегда компактнее фиксированного 4-байтного int. Аналог используется в Google Protocol Buffers и DWARF debug format. LEB128 декодирование чуть медленнее, но экономия трафика/памяти стоит того для network-delivered WASM.
Почему JVM имеет отдельные opcodes `iadd`, `fadd`, `ladd` вместо одного `add`?
Interpreter Dispatch
Dispatch - механизм выбора обработчика для каждого opcode в интерпретаторе. Это горячий путь: каждая инструкция проходит через dispatch. Три основных подхода: switch-dispatch, direct threading, indirect threading.
Threaded code впервые использован в Forth (1970) - по сути direct threading с array of pointers. CPython до 3.6 использовал switch-dispatch; с 3.6 добавлена поддержка computed goto на GCC/Clang (флаг `--with-computed-gotos`). Ускорение ~15-20%. Spidermonkey (Firefox JS) и V8 используют байткод только как fallback - горячий код JIT-компилируется в нативный и dispatch исчезает полностью.
Почему direct threading быстрее switch-dispatch?
Итоги
- **Stack VM** (JVM, CPython, WASM): компактный байткод, простая верификация, implicit операнды. Stack VP - проще реализовать.
- **Register VM** (Lua, Dalvik): explicit регистры, меньше инструкций, быстрее из-за меньшего числа dispatches.
- **Bytecode design** определяет: ширину инструкций, constant pool, типизацию opcodes, control flow (goto vs structured). Компромисс между компактностью, верификацией, JIT-friendliness.
- **Dispatch**: switch (простой), direct threading (быстрее через per-site branch prediction), superinstructions (слияние частых пар).
Связанные темы
Байткод и VM - основа управляемых сред исполнения:
- JVM: архитектура и байткод — JVM - наиболее полная реализация stack-based VM с верификацией и JIT
- V8: JavaScript — V8 Ignition использует register-based bytecode; Turbofan JIT компилирует горячие пути
- Линковка и загрузка — Загрузка .class и .wasm файлов - аналог динамической линковки для управляемых сред
Вопросы для размышления
- WASM запрещает goto и требует structured control flow (block/loop/if). Что именно это даёт верификатору - и как компилятор генерирует WASM из языка с произвольными goto (например, из C через Emscripten)?
- CPython использует adaptive specialization с 3.11. Что происходит при deoptimization - когда тип аргумента меняется после специализации? Насколько часто это встречается в реальном Python коде?
- Lua VM однопоточная, JVM многопоточная. Как многопоточность усложняет дизайн bytecode и interpreter dispatch - что нужно добавить к однопоточной VM?