6.2. Языковые виртуальные машины
Языковые виртуальные машины предназначены для исполнения программ на конкретном языке программирования (или семействе таких языков) в условиях различных программно-аппаратных платформах без перекомпиляции. Иными словами, упрощается портирование программ.
Рассмотрим классический пример П-кода (P-code) для виртуального выполнения программ на языка Паскаль, предложенный в середине 70-х годов. Специальный вариант компилятора Паскаля порождает платформонезависимый П-код. Для различных программно-аппаратных платформ реализованы интерпретаторы П-кода, также включающие библиотеку времени выполнения. В результате компилятор существует только в одном варианте и для каждой из новых целевых платформ достаточно реализовать небольшую программу – интерпретатор П-кода.
К ярким историческим примерам успешного портирования компьютерных игр на множество различных игровых платформ относятся текстовая игра Zork (1978), реализованная в коде виртуальной Z-машины в 1979 году, а также игра Another World (1991), имеющая изощренную виртуальную машину с поддержкой многопоточности, графики и звука.
К популярным современным языковым виртуальным машинам можно отнести:
- Java Virtual Machine (JVM). Виртуальная машина для Java.
- Common Language Runtime (CLR). Виртуальная машина среды .NET.
- CPython Virtual Machine. Виртуальная машина языка Python.
- WebAssembly (WASM). Виртуальная машина для веб-приложений.
Языковая виртуальная машина выполняет команды абстрактного процессора, ориентированного на конструкции конкретного языка или целого семейства языков. Программу в таком представлении принято называть байткодом. Такое название закрепилось исторически, оно связано с виртуальной машиной языка Smalltalk, в которой большинство команд кодировалось одним байтом.
Поскольку реальные процессоры, в большинстве случаев, не поддерживают на аппаратном уровне исполнение байткода, то используется программная модель языковой машины.
Архитектуры виртуальных машин, как и архитектуры реальных процессоров, можно разделить на два класса:
- стековые машины,
- регистровые машины.
Рассмотрим байткод различных виртуальных машин для вычисления выражения
$$b b - 4 a c.$$
В стековой модели вычислений это выражение представляется в постфиксной форме записи:
b b * 4 a * c * -
В регистровой модели вычислений то же выражение может быть представлено трехадресным кодом:
t1 = b * b
t2 = 4 * a
t3 = t2 * c
t4 = t1 - t3
В JVM используется стековая модель вычислений:
0: iload_1
1: iload_1
2: imul
3: iconst_4
4: iload_0
5: imul
6: iload_2
7: imul
8: isub
9: ireturn
В CPython тоже используется стековая модель вычислений:
0 LOAD_FAST 1 (b)
2 LOAD_FAST 1 (b)
4 BINARY_MULTIPLY
6 LOAD_CONST 1 (4)
8 LOAD_FAST 0 (a)
10 BINARY_MULTIPLY
12 LOAD_FAST 2 (c)
14 BINARY_MULTIPLY
16 BINARY_SUBTRACT
18 RETURN_VALUE
Виртуальная машина языка Lua использует регистровую вычислительную модель (см. числа после имен операций):
1 MUL 3 1 1
2 MMBIN 1 1 8 ; __mul
3 MULK 4 0 0 ; 4
4 MMBINK 0 0 8 1 ; __mul 4 flip
5 MUL 4 4 2
6 MMBIN 4 2 8 ; __mul
7 SUB 3 3 4
8 MMBIN 3 4 7 ; __sub
9 RETURN1 3
10 RETURN0
Во многих случаях для выполнения байткода быстродействия интерпретатора оказывается недостаточно и может использоваться трансляция. Различают следующие варианты трансляции:
- AOT (Ahead-of-Time). Полная трансляция байткода в машинный код целевой платформы перед ее запуском.
- JIT (Just-in-Time). Динамическая трансляция областей программы прямо во время интерпретации байткода.