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

Материал из Википедии — свободной энциклопедии
(перенаправлено с «Динамическая компиляция»)
Перейти к: навигация, поиск

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

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

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

Особенности реализации[править | править вики-текст]

JIT-компиляция может быть применена как ко всей программе, так и к её отдельным частям. Например, текстовый редактор может на лету компилировать регулярные выражения для более быстрого поиска по тексту. С AOT-компиляции такое сделать не представляется возможным, так как данные предоставляются во время исполнения программы, а не во время компиляции. JIT используется в реализациях Java, 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. Компиляция может осуществляться непосредственно для целевого CPU и операционной системы, на которой запущено приложение. Например, JIT может использовать векторные SSE2 расширения процессора, если он обнаружит их поддержку. Однако, до сих пор нет основных реализаций JIT, где этот подход бы использовался, ведь что-бы обеспечить подобный уровень оптимизации, сравнимый со статическими компиляторами, потребовалось бы либо поддерживать бинарный файл под каждую платформу, либо включать в одну библиотеку оптимизаторы под каждую платформу.
  2. Среда может собирать статистику о работающей программе и производить оптимизации с учётом этой информации. Некоторые статические компиляторы также могут принимать на вход информацию о предыдущих запусках приложения.
  3. Среда может делать глобальные оптимизации кода (например, встраивание библиотечных функций в код) без потери преимуществ динамической компиляции и без накладных расходов, присущим статических компиляторам и линкерам.
  4. Более простое перестраивание кода для лучшего использования кэша

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

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

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

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

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

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

История[править | править вики-текст]

Самую первую реализацию JIT можно отнести к LISP, написанную McCarthy in 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); для большей безопасности флаг должен быть поставлен после загрузки кода в память, а эта память должна быть помечена как доступная только для чтения, так как перезаписываемая и исполняемая память есть ничто иное, как дыра в безопасности.

См. также[править | править вики-текст]

Примечания[править | править вики-текст]

  1. Core Java: An Integrated Approach, p.12
  2. GNU lightning — GNU Project — Free Software Foundation (FSF)
  3. Benjamin Peterson — PyPy
  4. И снова про опасность eval()
  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
  10. Juice — OberonCore