JIT-компиляция

Материал из Википедии — свободной энциклопедии
Перейти к навигации Перейти к поиску

JIT-компиляция (англ. just-in-time, компиляция «точно в нужное время»), динамическая компиляция (англ. dynamic translation) — технология увеличения производительности программных систем, использующих байт-код, путём компиляции байт-кода в машинный код или в другой формат непосредственно во время работы программы. Таким образом достигается высокая скорость выполнения по сравнению с интерпретируемым байт-кодом[1] (сравнимая с компилируемыми языками) за счёт увеличения потребления памяти (для хранения результатов компиляции) и затрат времени на компиляцию. Технология JIT базируется на двух более ранних идеях, касающихся среды выполнения: компиляции байт-кода и динамической компиляции.

Так как JIT-компиляция является, по сути, одной из форм динамической компиляции, она позволяет применять такие технологии, как адаптивная оптимизация и динамическая рекомпиляция. Благодаря этому JIT-компиляция может показывать лучшие результаты в плане производительности, чем статическая компиляция. Интерпретация и JIT-компиляция особенно хорошо подходят для динамических языков программирования, при этом среда исполнения справляется с поздним связыванием типов и гарантирует безопасность исполнения.

Проекты LLVM, GNU Lightning[2], libJIT (часть проекта DotGNU) и RPython (часть проекта PyPy) могут быть использованы для создания JIT-интерпретаторов любого скриптового языка.

Особенности реализации

[править | править код]

JIT-компиляция может быть применена как ко всей программе, так и к её отдельным частям. Например, текстовый редактор может на лету компилировать регулярные выражения для более быстрого поиска по тексту. С AOT-компиляцией такое сделать не представляется возможным для случаев, когда данные предоставляются во время исполнения программы, а не в момент компиляции. JIT используется в реализациях Java (JRE), JavaScript, .NET Framework, в одной из реализаций Python — PyPy.[3] Существующие наиболее распространённые интерпретаторы языков PHP, Ruby, Perl, Python и им подобных имеют ограниченные или неполные JIT.

Большинство реализаций JIT имеет последовательную структуру: сначала приложение компилируется в байт-код виртуальной машины среды исполнения (AOT-компиляция), а потом JIT компилирует байт-код непосредственно в машинный код. В итоге при запуске приложения тратится лишнее время, что впоследствии компенсируется более быстрой его работой.

В языках, таких как Java, PHP, C#, Lua, Perl, GNU CLISP, исходный код транслируется в одно из промежуточных представлений, называемое байт-кодом. Байт-код не является машинным кодом какого-либо конкретного процессора и может переноситься на различные компьютерные архитектуры и исполняться точно так же. Байт-код интерпретируется (исполняется) виртуальной машиной. JIT читает байт-код из некоторых секторов (редко сразу из всех) и компилирует их в машинный код. Этим сектором может быть файл, функция или любой фрагмент кода. Единожды скомпилированный код может кэшироваться и в дальнейшем повторно использоваться без перекомпиляции.

Динамически компилируемая среда — это среда, в которой компилятор может вызываться приложением во время выполнения. Например, большинство реализаций Common Lisp содержат функцию compile, которая может создать функцию во время выполнения; в Python это функция eval. Это удобно для программиста, так как он может контролировать, какие части кода действительно подлежат компиляции. Также с помощью этого приёма можно компилировать динамически сгенерированный код, что в некоторых случаях приводит даже к лучшей производительности, чем реализация в статически скомпилированном коде. Однако стоит помнить, что подобные функции могут быть опасны, особенно когда данные передаются из недоверенных источников.[4]

Основная цель использования JIT — достичь и превзойти производительность статической компиляции, сохраняя при этом преимущества динамической компиляции:

  • Большинство тяжеловесных операций, таких, как парсинг исходного кода и выполнение базовых оптимизаций, происходит во время компиляции (до развёртывания), в то время как компиляция в машинный код из байт-кода происходит быстрее, чем из исходного кода.
  • Байт-код более переносим (в отличие от машинного кода).
  • Среда может контролировать выполнение байт-кода после компиляции, поэтому приложение может быть запущено в песочнице (для нативных программ такая возможность тоже существует, но реализация данной технологии сложнее).
  • Компиляторы из байт-кода в машинный код легче в реализации, так как большинство работы по оптимизации уже было проделано компилятором.

JIT, как правило, эффективней, чем интерпретация кода. К тому же в некоторых случаях JIT может показывать большую производительность по сравнению со статической компиляцией за счёт оптимизаций, возможных только во время исполнения:

  1. Компиляция может осуществляться непосредственно для целевого процессора и операционной системы, на которой запущено приложение. Например, JIT может использовать векторные SSE2 расширения процессора, если он обнаружит их поддержку.
  2. Среда может собирать статистику о работающей программе и производить оптимизации с учётом этой информации. Некоторые статические компиляторы также могут принимать на вход информацию о предыдущих запусках приложения.
  3. Среда может делать глобальные оптимизации кода (например, встраивание библиотечных функций в код) без потери преимуществ динамической компиляции и без накладных расходов, присущих статическим компиляторам и компоновщикам.
  4. Более простое перестраивание кода для лучшего использования кэша.

Задержка при запуске, средства борьбы с ней

[править | править код]

Типичная причина задержки при запуске JIT-компилятора — расходы на загрузку среды и компиляцию приложения в машинный код. В общем случае, чем лучше и чем больше оптимизаций выполняет JIT, тем больше получается задержка. Поэтому разработчикам JIT приходится искать компромисс между качеством генерируемого кода и временем запуска. Однако, часто оказывается так, что узким местом в процессе компиляции оказывается не сам процесс компиляции, а задержки системы ввода-вывода (так, например, rt.jar в Java Virtual Machine (JVM) имеет размер 40 МБ, и поиск метаданных в нём занимает достаточно большое количество времени).

Ещё одно средство оптимизации — компилировать только те участки приложения, которые используются чаще всего. Этот подход реализован в PyPy и HotSpot Java Virtual Machine компании Sun Microsystems.

В качестве эвристики может использоваться счётчик запусков участков приложения, размер байт-кода или детектор циклов.

Порой достаточно сложно найти правильный компромисс. Так, например, Sun’s Java Virtual Machine имеет два режима работы — клиент и сервер. В режиме клиента количество компиляций и оптимизаций минимально для более быстрого запуска, в то время как в режиме сервера достигается максимальная производительность, но из-за этого увеличивается время запуска.

Ещё одна техника, называемая pre-JIT, компилирует код до запуска. Преимуществом данной техники является уменьшенное время запуска, в то же время недостатком является плохое качество скомпилированного кода по сравнению с runtime JIT.

Самую первую реализацию JIT можно отнести к LISP, написанную McCarthy в 1960 году[5]. В его книге Recursive functions of symbolic expressions and their computation by machine, Part I он упоминает функции, компилируемые во время выполнения, тем самым избавив от надобности вывода работы компилятора на перфокарты.

Другой ранний пример упоминания JIT можно отнести к Кену Томпсону, который в 1968 году впервые применил регулярные выражения для поиска подстрок в текстовом редакторе QED. Для ускорения алгоритма Томпсон реализовал компиляцию регулярных выражений в машинный код IBM 7094.

Метод получения скомпилированного кода был предложен Митчелом в 1970 году, когда он реализовал экспериментальный язык LC2.[6][7]

Smalltalk (1983) был пионером в области JIT-технологий. Трансляция в машинный код выполнялась по требованию и кэшировалась для дальнейшего использования. Когда память кончалась, система могла удалить некоторую часть кэшированного кода из оперативной памяти и восстановить его, когда он снова потребуется. Язык программирования Self некоторое время был самой быстрой реализацией Smalltalk и работал всего лишь в два раза медленней C, будучи полностью объектно-ориентированным.

Self был заброшен Sun, но исследования продолжились в рамках языка Java. Термин «Just-in-time компиляция» был заимствован из производственного термина «Точно в срок» и популяризован Джеймсом Гослингом, использовавшим этот термин в 1993.[8] В данный момент JIT используется почти во всех реализациях Java Virtual Machine.

Также большой интерес представляет диссертация, защищённая в 1994 году в Университете ETH (Швейцария, Цюрих) Михаэлем Францем «Динамическая кодогенерация — ключ к переносимому программному обеспечению»[9] и реализованная им система Juice[10] динамической кодогенерации из переносимого семантического дерева для языка Оберон. Система Juice предлагалась как плагин для интернет-браузеров.

Безопасность

[править | править код]

Так как JIT составляет исполняемый код из данных, возникает вопрос безопасности и возможных уязвимостей.

JIT компиляция включает в себя компиляцию исходного кода или байт-кода в машинный код и его выполнение. Как правило, результат записывается в память и исполняется сразу же, без промежуточного сохранения на диск или его вызов как отдельной программы. В современных архитектурах для повышения безопасности произвольные участки памяти не могут быть исполнены как машинный код (NX bit). Для корректного запуска регионы памяти должны быть предварительно помечены как исполняемые, при этом для большей безопасности флаг исполнения может ставиться только после снятия флага разрешения записи (Схема защиты W^X)[11].

Примечания

[править | править код]
  1. Core Java: An Integrated Approach Архивная копия от 27 августа 2017 на Wayback Machine, 2008, ISBN 9788177228366, Dreamtech Press, 2008. p.12
  2. GNU lightning — GNU Project — Free Software Foundation (FSF). Дата обращения: 27 августа 2017. Архивировано 19 сентября 2017 года.
  3. Benjamin Peterson — PyPy Архивная копия от 12 мая 2008 на Wayback Machine
  4. И снова про опасность eval() Архивная копия от 13 сентября 2014 на Wayback Machine, habrahabr
  5. Aycock 2003, 2. JIT Compilation Techniques, 2.1 Genesis, p. 98.
  6. Aycock 2003, 2. JIT Compilation Techniques, 2.2 LC², p. 98-99.
  7. Mitchell, J.G. (1970). The design and construction of flexible and efficient interactive programming systems.
  8. Aycock & 2003 2.14 Java, p. 107, footnote 13.
  9. Михаэль Франц — OberonCore Архивная копия от 26 сентября 2017 на Wayback Machine; dissertation, entitled "Code Generation On-The-Fly: A Key To Portable Software," Архивная копия от 7 сентября 2017 на Wayback Machine
  10. Juice — OberonCore. Дата обращения: 7 ноября 2009. Архивировано 23 декабря 2009 года.
  11. Write XOR Execute JIT Support Lands For Mozilla Firefox Архивная копия от 2 августа 2017 на Wayback Machine / Phoronix, 4 January 2016  (англ.)