обзор
Интерпретатор для языка X представляет собой программу (или машину, или просто какой - то механизм в целом) , который выполняет любую программу р , написанный на языке X таким образом, что он выполняет эффекты и оценивает результаты , как это предписано спецификацией X . Процессоры обычно являются интерпретаторами для их соответствующих наборов инструкций, хотя современные высокопроизводительные процессоры рабочих станций на самом деле более сложны, чем это; на самом деле они могут иметь собственный частный набор частных команд и либо переводить (компилировать), либо интерпретировать видимый снаружи набор общих команд.
Компилятор от X к Y представляет собой программу (или машина, или просто какой - то механизм в целом) , который переводит любую программу р из некоторого языка X в семантически эквивалентной программы р ' на некотором языке Y таким образом , что семантика программы сохраняются, т.е. , что интерпретация р ' с переводчиком Y даст те же результаты , и имеют те же эффекты , как интерпретировать р с переводчиком X . (Обратите внимание, что X и Y могут быть одним и тем же языком.)
Термины Ahead-of-Time (AOT) и Just-in-Time (JIT) относятся к моменту, когда происходит компиляция: «время», упомянутое в этих терминах, является «временем выполнения», то есть компилятор JIT компилирует программу так, как она есть. работает , AOT-компилятор компилирует программу до ее запуска . Обратите внимание, что для этого требуется, чтобы JIT-компилятор с языка X на язык Y каким-то образом работал вместе с интерпретатором для языка Yиначе не было бы способа запустить программу. (Так, например, JIT-компилятор, который компилирует JavaScript в машинный код x86, не имеет смысла без процессора x86; он компилирует программу во время работы, но без процессора x86 программа не будет работать.)
Обратите внимание, что это различие не имеет смысла для переводчиков: переводчик запускает программу. Идея интерпретатора AOT, который запускает программу до ее запуска, или интерпретатора JIT, который запускает программу во время ее работы, бессмысленна.
Итак, имеем:
- Компилятор AOT: компилируется перед запуском
- JIT-компилятор: компилируется во время работы
- переводчик: работает
JIT Компиляторы
В семействе JIT-компиляторов все еще есть много различий относительно того, когда именно они компилируются, как часто и с какой степенью детализации.
Например, JIT-компилятор в Microsoft CLR компилирует код только один раз (при его загрузке) и компилирует целую сборку за раз. Другие компиляторы могут собирать информацию во время работы программы и несколько раз перекомпилировать код по мере появления новой информации, которая позволяет им лучше оптимизировать ее. Некоторые JIT-компиляторы даже способны де-оптимизировать код. Теперь вы можете спросить себя, почему кто-то захочет это сделать? Де-оптимизация позволяет вам выполнять очень агрессивную оптимизацию, которая может быть на самом деле небезопасной: если окажется, что вы были слишком агрессивны, вы можете просто вернуться обратно, тогда как с JIT-компилятором, который не может де-оптимизировать, вы не смогли бы запустить агрессивные оптимизации в первую очередь.
JIT-компиляторы могут либо скомпилировать некоторую статическую единицу кода за один раз (один модуль, один класс, одна функция, один метод,…; их обычно называют, например, методом JIT), либо они могут отслеживать динамические выполнение кода для поиска динамических трасс (как правило, циклов), которые они затем скомпилируют (это называется трассировкой JIT).
Объединение интерпретаторов и компиляторов
Интерпретаторы и компиляторы могут быть объединены в единый механизм исполнения языка. Есть два типичных сценария, где это делается.
Объединяя AOT компилятор из X в Y с переводчиком Y . Здесь, как правило, X - это язык более высокого уровня, оптимизированный для удобства чтения людьми, тогда как Yэто компактный язык (часто какой-то байт-код), оптимизированный для интерпретации на машинах. Например, механизм выполнения CPython Python имеет компилятор AOT, который компилирует исходный код Python в байт-код CPython, и интерпретатор, который интерпретирует байт-код CPython. Аналогично, механизм исполнения YARV Ruby имеет компилятор AOT, который компилирует исходный код Ruby в байт-код YARV, и интерпретатор, который интерпретирует байт-код YARV. Почему вы хотите это сделать? Ruby и Python являются очень высокоуровневыми и несколько сложными языками, поэтому мы сначала компилируем их в язык, который легче анализировать и легче интерпретировать, а затем интерпретируем этот язык.
Другой способ объединить интерпретатор и компилятор - это механизм исполнения в смешанном режиме . Здесь мы «смешивать» два «режима» от реализации такой же язык вместе, т.е. интерпретатор X и JIT компилятор из X в Y . (Таким образом, разница здесь заключается в том, что в приведенном выше случае у нас было несколько «этапов», когда компилятор компилировал программу и затем передавал результат в интерпретатор, здесь мы имеем два работающих бок о бок на одном языке. ) Код, скомпилированный компилятором, имеет тенденцию работать быстрее, чем код, выполняемый интерпретатором, но на самом деле компиляция кода сначала требует времени (и особенно, если вы хотите сильно оптимизировать код для запускаочень быстро, это занимает много времени). Таким образом, чтобы преодолеть это время, когда JIT-компилятор занят компиляцией кода, интерпретатор уже может начать выполнение кода, и как только JIT завершит компиляцию, мы можем переключить выполнение на скомпилированный код. Это означает, что мы получаем максимально возможную производительность скомпилированного кода, но нам не нужно ждать окончания компиляции, и наше приложение начинает работать сразу (хотя и не так быстро, как могло бы быть).
На самом деле это самое простое из возможных применений механизма исполнения в смешанном режиме. Более интересные возможности, например, не начинать компиляцию сразу, а позволить интерпретатору немного поработать и собрать статистику, информацию о профилировании, информацию о типе, информацию о вероятности того, какие конкретные условные переходы приняты, какие методы вызваны чаще всего и т. д., а затем передает эту динамическую информацию компилятору, чтобы он мог генерировать более оптимизированный код. Это также способ реализовать де-оптимизацию, о которой я говорил выше: если окажется, что вы слишком агрессивны в оптимизации, вы можете отбросить (часть) код и вернуться к интерпретации. JSM HotSpot делает это, например. Он содержит как интерпретатор для байт-кода JVM, так и компилятор для байт-кода JVM. (По факту,два компилятора!)
Также возможно , и в самом деле , общей для объединить эти два подхода: две фазы с первым будучи АОТ компилятор , который компилирует X в Y , а вторая фаза будучи смешанным режимом работы двигателя , что и интерпретирует Y и компилирует Y в Z . Например, механизм исполнения Rubinius Ruby работает следующим образом: у него есть компилятор AOT, который компилирует исходный код Ruby в байт-код Rubinius, и механизм смешанного режима, который сначала интерпретирует байт-код Rubinius, а после сбора некоторой информации компилирует наиболее часто вызываемые методы в нативный Машинный код.
Обратите внимание, что роль, которую интерпретатор играет в случае механизма выполнения смешанного режима, а именно обеспечения быстрого запуска, а также потенциального сбора информации и обеспечения возможности резервирования, может альтернативно также выполняться вторым JIT-компилятором. Так работает V8, например. V8 никогда не интерпретирует, он всегда компилируется. Первый компилятор - очень быстрый, очень тонкий компилятор, который запускается очень быстро. Код, который он выдает, не очень быстрый. Этот компилятор также внедряет код профилирования в код, который он генерирует. Другой компилятор работает медленнее и использует больше памяти, но генерирует гораздо более быстрый код и может использовать информацию профилирования, собранную путем запуска кода, скомпилированного первым компилятором.