Библиотека сайта rus-linux.net
Среда времени выполнения динамических языков и языки Iron
Глава 8 из книги "Архитектура приложений с открытым исходным кодом", том 2.
Оригинал: The Dynamic Language Runtime and the Iron Languages
Автор: Jeff Hardy
Перевод: Н.Ромоданов
8.6. Интерпретация и компиляция
Одним из недостатков использования компилятора JIT, например, применяемого на платформе .NET, является то, что ему при запуске требуется время для того, чтобы преобразовать байт-код IL в машинный код, который может исполнять процессор. При JIT-компиляции создается код, который работает намного быстрее, чем при работе через интерпретатор, но в зависимости того, что требуется сделать, затраты на запуск могут оказаться непомерно высокими. К примеру, долгоживущий серверный процесс, например, веб-приложение, выиграет от использования JIT, поскольку время запуска почти не имеет значения, а время, необходимое для на каждый запрос, является решающим, причем один и тот же код выполняется повторно. С другой стороны, для программы, которая запускается часто, но только на короткий промежуток времени, например, для клиентской программы Mercurial, работающей из командной строки, было бы лучше иметь небольшое время запуска, поскольку в ней, скорее всего, каждый фрагмент кода выполняется только один раз, а то, что код, созданный компилятором JIT, работает быстрее, не скомпенсирует того, что на запуск потребуется гораздо больше времени.
Платформа .NET не может выполнять код IL непосредственно; она всегда использует компилятор JIT для компиляции в машинный код, а на это требуется время. В частности, время запуска программ является одной из слабых сторон фреймворка .NET, поскольку компилятору JIT нужно откомпилировать большую часть кода. Хотя есть способы, которые позволяют избегать подобных затрат для статических программ платформы .NET (генерация нативных образов - Native Image Generation, или NGEN), для динамических программ они не работают. Вместо того, чтобы выполнять компиляцию всегда непосредственно в код IL, в языках IronRuby и IronPython будут использоваться их собственные интерпретаторы (находится в Microsoft.Scripting.Interpreter
), которые работаю не так быстро, как код, откомпилированный с помощью JIT, но при запуске затрачивают гораздо меньше времени. Интерпретатор также полезен в ситуациях, когда не допускается динамическая генерация кода, например, на мобильных платформах; в противном случае языки DLR вообще не смогут работать.
Перед тем, как начнется выполнение, все дерево выражений должно быть преобразовано в функцию так, что его можно было выполнить. В языках DLR функции представлены в виде узлов LambdaExpression
. В большинстве языков функция lambda является анонимной функцией, а в языках DLR концепция именования не используется; все функции являются анонимными. Узел LambdaExpression
уникален тем, что это единственный тип узла, который может быть преобразован в делегата delegate, т. е. в такую сущность, которая на платформе .NET с использованием метода Compile()
вызывает функции первого порядка. Делегат похож на указатель функций в языке C - это просто дескриптор куска кода, который можно вызывать.
Сначала дерево выражения помещается в узел LightLambdaExpression
, из которого можно также создать делегата, который может быть выполнен; однако вместо того, чтобы генерировать код на языке IL (для которого затем нужно будет вызывать компилятор JIT), дерево выражений компилируется в список инструкций, которые затем выполняются на простой виртуальной машине интерпретатора. Интерпретатор является простым стековым интерпретатором; инструкции берут значения из стека, выполняют операцию, а затем помещают результат обратно в стек. Каждая команда представляет собой экземпляр класса, производного от класса Microsoft.Scripting.Interpreter.Instruction
(например, AddInstruction
или BranchTrueInstruction
), в котором есть свойства, описывающие, сколько элементов выбирается из стека, сколько из них используется, и метод Run()
, который выполняет инструкцию, выбирая значения из стека, помещая результат в стек и возвращая смещение для следующей инструкции. Интерпретатор берет список инструкций и выполняет их одну за другой, переходя вперед или назад в зависимости от значения, возвращаемого методом Run()
.
Как только некоторая часть кода будет выполнена определенное количество раз, она будет с помощью вызова метода LightLambdaExpression.Reduce()
преобразована в полный вариант узла LambdaExpression
, затем будет выполнена компиляция в делегата DynamicMethod
(в фоновом потоке с целью определенного распараллеливания), и точка вызова старого делегата будут заменена новым вызовом, более быстрым. Это значительно снижает стоимость выполнения функций, вызываемых всего-лишь несколько раз, например, функция main программы, тогда как функции, к которым происходит регулярное обращение, работают настолько быстро, насколько это возможно. По умолчанию, порог компиляция установлен равным 32 кратному исполнению кода, но это значение можно изменять с помощью параметра командной строки или из хост-программы; также можно полностью отключить либо компиляцию, либо интерпретацию.
Независимо от того, работает ли интерпретатор, или происходит компиляция в код IL, компилятор дерева выражений не выполняет непосредственное преобразование в операции языка. Вместо этого компилятор создает для каждой операции, которая может быть динамической (что бывает практически всегда), точку вызова. Благодаря таким точкам вызовов можно использовать динамические объекты, а код сохранит высокую производительность.
Продолжение статьи: Точки динамических вызовов