В вычислительной технике компиляция « точно в срок» ( JIT ) (также динамическая трансляция или компиляция во время выполнения ) [1] — это компиляция ( компьютерного кода ) во время выполнения программы (во время выполнения ), а не перед выполнением. [2] Это может состоять из трансляции исходного кода, но чаще всего это трансляция байт-кода в машинный код , который затем выполняется напрямую. Система, реализующая JIT-компилятор, обычно непрерывно анализирует исполняемый код и определяет части кода, где ускорение, полученное от компиляции или перекомпиляции, перевешивает накладные расходы на компиляцию этого кода.
JIT-компиляция представляет собой комбинацию двух традиционных подходов к трансляции в машинный код — предварительной компиляции (AOT) и интерпретации — и объединяет некоторые преимущества и недостатки обоих. [2] Грубо говоря, JIT-компиляция объединяет скорость скомпилированного кода с гибкостью интерпретации, с накладными расходами интерпретатора и дополнительными накладными расходами на компиляцию и связывание (не только интерпретацию). JIT-компиляция является формой динамической компиляции и допускает адаптивную оптимизацию, такую как динамическая перекомпиляция и ускорения, специфичные для микроархитектуры . [nb 1] [3] Интерпретация и JIT-компиляция особенно подходят для динамических языков программирования , поскольку система выполнения может обрабатывать типы данных с поздним связыванием и обеспечивать гарантии безопасности.
Самый ранний опубликованный JIT-компилятор обычно приписывается работе Джона Маккарти над LISP в 1960 году. [4] В своей основополагающей статье Рекурсивные функции символических выражений и их вычисление машиной, часть I он упоминает функции, которые транслируются во время выполнения, тем самым избавляя от необходимости сохранять вывод компилятора на перфокартах [5] (хотя это было бы более точно называть « системой Compile and go »). Другим ранним примером был Кен Томпсон , который в 1968 году дал одно из первых применений регулярных выражений , здесь для сопоставления с образцом в текстовом редакторе QED . [6] Для скорости Томпсон реализовал сопоставление регулярных выражений с помощью JIT для кода IBM 7094 на Compatible Time-Sharing System . [4] Влиятельная техника получения скомпилированного кода из интерпретации была впервые предложена Джеймсом Г. Митчеллом в 1970 году, и он реализовал ее для экспериментального языка LC² . [7] [8]
Smalltalk (ок. 1983) стал пионером новых аспектов JIT-компиляции. Например, перевод в машинный код выполнялся по требованию, а результат кэшировался для последующего использования. Когда памяти становилось мало, система удаляла часть этого кода и восстанавливала его, когда он снова был нужен. [2] [9] Язык Self компании Sun значительно улучшил эти методы и в какой-то момент стал самой быстрой системой Smalltalk в мире, достигая до половины скорости оптимизированного C [10], но с полностью объектно-ориентированным языком.
Self был заброшен Sun, но исследования перешли на язык Java. Термин «Just-in-time compilation» был заимствован из производственного термина « Just in time » и популяризирован Java, Джеймс Гослинг использовал этот термин с 1993 года. [11] В настоящее время JITing используется большинством реализаций виртуальной машины Java , поскольку HotSpot строится на этой исследовательской базе и широко использует ее.
Проект HP Dynamo был экспериментальным JIT-компилятором, в котором формат «байт-кода» и формат машинного кода были одинаковыми; система оптимизировала машинный код PA-8000 . [12] Как ни странно, это привело к ускорению, в некоторых случаях на 30%, поскольку это позволило оптимизировать работу на уровне машинного кода, например, встроить код для лучшего использования кэша и оптимизировать вызовы динамических библиотек, а также выполнить множество других оптимизаций во время выполнения, которые обычные компиляторы не в состоянии выполнить. [13] [14]
В ноябре 2020 года в PHP 8.0 появился JIT-компилятор. [15]
В системе, компилируемой в байт-код, исходный код транслируется в промежуточное представление, известное как байт-код . Байт-код не является машинным кодом для какого-либо конкретного компьютера и может быть переносимым между архитектурами компьютеров. Затем байт-код может быть интерпретирован или запущен на виртуальной машине . JIT-компилятор считывает байт-коды во многих разделах (или полностью, редко) и динамически компилирует их в машинный код, чтобы программа могла работать быстрее. Это можно сделать для каждого файла, для каждой функции или даже для любого произвольного фрагмента кода; код может быть скомпилирован, когда он должен быть выполнен (отсюда и название «точно вовремя»), а затем кэширован и повторно использован позже без необходимости перекомпиляции.
Напротив, традиционная интерпретируемая виртуальная машина просто интерпретирует байт-код, как правило, с гораздо более низкой производительностью. Некоторые интерпретаторы даже интерпретируют исходный код без этапа предварительной компиляции в байт-код, с еще более низкой производительностью. Статически скомпилированный код или машинный код компилируется до развертывания. Динамическая среда компиляции — это среда, в которой компилятор может использоваться во время выполнения. Распространенной целью использования методов JIT является достижение или превзойти производительность статической компиляции , сохраняя при этом преимущества интерпретации байт-кода: Большая часть «тяжелой работы» по анализу исходного кода и выполнению базовой оптимизации часто выполняется во время компиляции, до развертывания: компиляция из байт-кода в машинный код намного быстрее, чем компиляция из исходного кода. Развернутый байт-код является переносимым, в отличие от машинного кода. Поскольку среда выполнения контролирует компиляцию, как и интерпретируемый байт-код, он может работать в безопасной песочнице. Компиляторы из байт-кода в машинный код проще писать, потому что переносимый компилятор байт-кода уже выполнил большую часть работы.
JIT-код обычно предлагает гораздо лучшую производительность, чем интерпретаторы. Кроме того, в некоторых случаях он может предложить лучшую производительность, чем статическая компиляция, поскольку многие оптимизации возможны только во время выполнения: [16] [17]
Поскольку JIT должен визуализировать и выполнять собственный двоичный образ во время выполнения, истинные JIT машинного кода требуют платформ, которые позволяют выполнять данные во время выполнения, что делает использование таких JIT на машине с архитектурой Гарварда невозможным; то же самое можно сказать и о некоторых операционных системах и виртуальных машинах. Однако особый тип "JIT" может потенциально не быть нацелен на архитектуру ЦП физической машины, а скорее на оптимизированный байт-код виртуальной машины, где преобладают ограничения на необработанный машинный код, особенно там, где виртуальная машина этого байт-кода в конечном итоге использует JIT для собственного кода. [18]
JIT вызывает небольшую или заметную задержку при начальном выполнении приложения из-за времени, необходимого для загрузки и компиляции входного кода. Иногда эту задержку называют «задержкой времени запуска» или «временем прогрева». В общем, чем больше оптимизации выполняет JIT, тем лучше код, который он сгенерирует, но начальная задержка также увеличится. Поэтому JIT-компилятор должен найти компромисс между временем компиляции и качеством кода, который он надеется сгенерировать. Время запуска может включать в себя увеличение операций ввода-вывода в дополнение к JIT-компиляции: например, файл данных класса rt.jar для виртуальной машины Java (JVM) составляет 40 МБ, и JVM должна искать много данных в этом контекстно огромном файле. [19]
Одна из возможных оптимизаций, используемая HotSpot Java Virtual Machine от Sun, заключается в объединении интерпретации и JIT-компиляции. Код приложения изначально интерпретируется, но JVM отслеживает, какие последовательности байт-кода часто выполняются, и транслирует их в машинный код для непосредственного выполнения на оборудовании. Для байт-кода, который выполняется всего несколько раз, это экономит время компиляции и сокращает начальную задержку; для часто выполняемого байт-кода JIT-компиляция используется для запуска на высокой скорости после начальной фазы медленной интерпретации. Кроме того, поскольку программа тратит большую часть времени на выполнение меньшей части своего кода, сокращение времени компиляции имеет большое значение. Наконец, во время первоначальной интерпретации кода статистика выполнения может быть собрана до компиляции, что помогает выполнить лучшую оптимизацию. [20]
Правильный компромисс может меняться в зависимости от обстоятельств. Например, виртуальная машина Java от Sun имеет два основных режима — клиент и сервер. В клиентском режиме выполняется минимальная компиляция и оптимизация для сокращения времени запуска. В серверном режиме выполняется обширная компиляция и оптимизация для максимизации производительности после запуска приложения за счет сокращения времени запуска. Другие компиляторы Java just-in-time использовали измерение времени выполнения количества выполнений метода в сочетании с размером байт-кода метода в качестве эвристики для принятия решения о том, когда компилировать. [21] Еще один использует количество выполнений в сочетании с обнаружением циклов. [22] В общем случае гораздо сложнее точно предсказать, какие методы оптимизировать в краткосрочных приложениях, чем в долгосрочных. [23]
Native Image Generator (Ngen) от Microsoft — еще один подход к сокращению начальной задержки. [24] Ngen предварительно компилирует (или «pre-JITs») байт-код в образе Common Intermediate Language в машинный машинный код. В результате компиляция во время выполнения не требуется. .NET Framework 2.0, поставляемый с Visual Studio 2005, запускает Ngen на всех библиотеках DLL Microsoft сразу после установки. Предварительная компиляция позволяет сократить время запуска. Однако качество генерируемого кода может быть не таким хорошим, как у кода, обработанного JIT, по тем же причинам, по которым код, скомпилированный статически, без оптимизации с управлением профилем , не может быть таким же хорошим, как код, скомпилированный JIT, в крайнем случае: отсутствие данных профилирования для управления, например, встроенным кэшированием. [25]
Существуют также реализации Java, которые объединяют компилятор AOT (ahead-of-time) с JIT-компилятором ( Excelsior JET ) или интерпретатором ( GNU Compiler for Java ).
JIT-компиляция может не достичь своей цели, а именно, войти в устойчивое состояние улучшенной производительности после короткого начального периода прогрева. [26] [27] На восьми различных виртуальных машинах Барретт и др. (2017) измерили шесть широко используемых микротестов, которые обычно используются разработчиками виртуальных машин в качестве целей оптимизации, многократно запуская их в рамках одного выполнения процесса. [28] На Linux они обнаружили, что от 8,7% до 9,6% выполнений процессов не смогли достичь устойчивого состояния производительности, от 16,7% до 17,9% вошли в устойчивое состояние сниженной производительности после периода прогрева, и 56,5% пар определенной виртуальной машины, на которой запущен определенный тест, не смогли последовательно увидеть устойчивое состояние без ухудшения производительности в ходе нескольких выполнений (т. е. по крайней мере одно выполнение не смогло достичь устойчивого состояния или увидело снижение производительности в устойчивом состоянии). Даже там, где было достигнуто улучшенное устойчивое состояние, иногда для этого требовалось много сотен итераций. [29] Трайни и др. (2022) вместо этого сосредоточились на виртуальной машине HotSpot, но с гораздо более широким набором тестов, [30] обнаружив, что 10,9% выполнений процессов не смогли достичь устойчивого состояния производительности, а 43,5% тестов не смогли последовательно достичь устойчивого состояния при нескольких выполнениях. [31]
JIT-компиляция по сути использует исполняемые данные, что создает проблемы безопасности и возможные уязвимости.
Реализация JIT-компиляции состоит из компиляции исходного кода или байт-кода в машинный код и его выполнения. Обычно это делается непосредственно в памяти: JIT-компилятор выводит машинный код непосредственно в память и немедленно выполняет его, а не выводит его на диск и затем вызывает код как отдельную программу, как при обычной предварительной компиляции. В современных архитектурах это приводит к проблеме из-за защиты исполняемого пространства : произвольная память не может быть выполнена, так как в противном случае существует потенциальная дыра в безопасности. Таким образом, память должна быть помечена как исполняемая; по соображениям безопасности это должно быть сделано после того, как код был записан в память, и помечено как только для чтения, так как записываемая/исполняемая память является дырой в безопасности (см. W^X ). [32] Например, JIT-компилятор Firefox для Javascript представил эту защиту в версии выпуска Firefox 46. [33]
JIT-распыление — это класс эксплойтов компьютерной безопасности , которые используют JIT-компиляцию для распыления кучи : полученная память затем становится исполняемой, что позволяет использовать эксплойт, если выполнение можно перенести в кучу.
JIT-компиляция может применяться к некоторым программам или может использоваться для определенных возможностей, в частности динамических возможностей, таких как регулярные выражения . Например, текстовый редактор может компилировать регулярное выражение, предоставленное во время выполнения, в машинный код, чтобы обеспечить более быстрое сопоставление: это нельзя сделать заранее, так как шаблон предоставляется только во время выполнения. Несколько современных сред выполнения полагаются на JIT-компиляцию для высокоскоростного выполнения кода, включая большинство реализаций Java , вместе с Microsoft .NET . Аналогично, многие библиотеки регулярных выражений поддерживают JIT-компиляцию регулярных выражений либо в байт-код, либо в машинный код. JIT - компиляция также используется в некоторых эмуляторах для перевода машинного кода с одной архитектуры ЦП на другую.
Распространенная реализация JIT-компиляции заключается в том, чтобы сначала выполнить AOT-компиляцию в байт-код ( код виртуальной машины ), известную как компиляция байт-кода , а затем выполнить JIT-компиляцию в машинный код (динамическая компиляция), а не интерпретацию байт-кода. Это повышает производительность выполнения по сравнению с интерпретацией за счет задержки из-за компиляции. JIT-компиляторы транслируют непрерывно, как и интерпретаторы, но кэширование скомпилированного кода минимизирует задержку при будущем выполнении того же кода во время данного запуска. Поскольку компилируется только часть программы, задержка значительно меньше, чем если бы вся программа была скомпилирована перед выполнением.
{{cite journal}}
: Цитировать журнал требует |journal=
( помощь ){{cite web}}
: CS1 maint: unfit URL (link)