Основной выигрыш в интерпретации AST по сравнению с байт-кодом - это стоимость отправки операций, для высокооптимизированных интерпретаторов это начинает становиться реальной проблемой.«Отправка» - это термин, используемый для описания издержек, необходимых для начала выполнения операции (например, арифметики, доступа к свойству и т. Д.).
Довольно обычный интерпретатор на основе AST будет выглядеть примерно так:
class ASTNode {
virtual double execute() = 0;
}
class NumberNode {
virtual double execute() { return m_value; }
double m_value;
}
class AddNode {
virtual double execute() { return left->execute() + right->execute(); }
}
Таким образом, выполнение кода для чего-то такого простого, как 1+1
, требует 3 виртуальных вызова.Виртуальные вызовы очень дороги (по большому счету) из-за множества косвенных посылок при вызове и общих затрат на совершение вызова.
В интерпретаторе байт-кода у вас естьдругая модель диспетчеризации - вместо виртуальных вызовов у вас есть цикл выполнения, похожий на:
while (1) {
switch (op.type) {
case op_add:
// Efficient interpreters use "registers" rather than
// a stack these days, but the example code would be more
// complicated
push(pop() + pop());
continue;
case op_end:
return pop();
}
}
Это все еще имеет достаточно высокую стоимость диспетчеризации по сравнению с собственным кодом, но намного быстрее, чем виртуальная диспетчеризация.Вы можете дополнительно улучшить производительность, используя расширение gcc, называемое «computed goto», которое позволяет удалить диспетчерскую диспетчеризацию, снижая общую стоимость диспетчеризации в основном до одной непрямой ветви.ряд дополнительных преимуществ перед интерпретаторами AST, в основном из-за способности байт-кода «напрямую» переходить в другие места, как на реальной машине, например, представить фрагмент кода, подобный следующему:
while (1) {
...statements...
if (a)
break;
else
continue;
}
Чтобы реализовать это правильно каждый раз, когда выполняется оператор, вам необходимо указать, должно ли выполнение оставаться в цикле или останавливаться, поэтому цикл выполнения становится примерно таким:
while (condition->execute() == true) {
for (i = 0; i < statements->length(); i++) {
result = statements[i]->execute();
if (result.type == BREAK)
break;
if (result.type == CONTINUE)
i = 0;
}
}
По мере добавления дополнительных форм потокаУправлять этой сигнализацией становится все дороже.Как только вы добавляете исключения (например, управление потоком данных, которое может происходить везде), вы начинаете нуждаться в проверке этих вещей в середине даже базовой арифметики, что приводит к увеличению накладных расходов.Если вы хотите увидеть это в реальном мире, я рекомендую вам взглянуть на спецификацию ECMAScript, где они описывают модель выполнения в терминах интерпретатора AST.
В интерпретаторе байт-кода эти проблемы в основном исчезают, так какбайт-код способен напрямую выражать поток управления, а не косвенно посредством сигнализации, например.continue
просто преобразуется в инструкцию перехода, и вы получаете эту стоимость только в том случае, если она действительно достигнута.
Наконец, интерпретатор AST по определению является рекурсивным и поэтому должен предотвращать переполнение системного стека, чтонакладывает очень жесткие ограничения на то, сколько вы можете использовать в своем коде, например:
1+(1+(1+(1+(1+(1+(1+(1+1)))))))
Имеет 8 уровней рекурсии (по крайней мере) в интерпретаторе - это может быть очень значительным расходом;более старые версии Safari (pre-SquirrelFish) использовали интерпретатор AST, и по этой причине JS было разрешено использовать только пару сотен уровней рекурсии по сравнению с 1000 в современных браузерах.