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

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

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

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

Применение

JIT-компиляция может быть использована в отдельных программах, или для реализации определенного динамического функционала, такого как регулярные выражения. Для примера, редактор может скомпилировать регулярное выражение, которое было введено во время работы программы, в быстрый машинный код — эту компиляцию невозможно провести заблаговременно, поскольку шаблон регулярного выражения вводится во время выполнения. Некоторые современные среды исполнения полагаются на JIT-компиляцию для повышения скорости работы кода. Примерами таких сред являются большинство имплементаций Java и .NET Framework. Схожим образом, многие библиотеки используют JIT-компиляцию для трансляции регулярных выражений в необходимый байт - или машинный код. JIT компиляция также используется в некоторых эмуляторах, с целью трансляции машинного кода процессоров одной архитектуры к машинному коду процессора другой архитектуры.

Привычная JIT-компилятора выполняет статическую компиляцию перед выполнением, получая байткод (код виртуальной машины), известный также как байткод компиляция, а после — выполняет компиляцию в машинный код (динамическая компиляция, или JIT-компиляция), вместо простого процесса интерпретации байткоду в машинный код. Это позволяет улучшить скорость выполнения кода (по сравнению с интерпретацией), ценой потери времени на компиляцию. Трансляция кода JIT-компилятором, так же как и интерпретатором, является непрерывным процессом, однако кэширование скомпилированного кода уменьшает задержку дальнейшего выполнения повторно использованного кода. Поскольку в этом случае компилируется только часть программы, задержка на компиляцию перед выполнением меньше, чем время компиляции всей программы.

История развития JIT-компиляции, ее основных подходов

Первым опубликованным JIT-компилятором считается работа Джона Маккарти над LISP в 1960.  В его статье «Recursive functions of symbolic expressions and their computation by machine, Part I» (англ. Рекурсивные функции символических выражений и их вычисление машинами, Часть 1), он вспоминает функции, которые транслируются во время работы программы, избегая необходимости сохранения исходного кода компилятора на перфокартах. (лучшим термином для данной системы будет «Система компиляции и запуска» (англ. compile and go system).

Другим ранним применением JIT-компиляции является работа Кена Томпсона, шаблонизированный поиск текстового редактора QED, в котором использовалась JIT-компиляция регулярных выражений в машинный код IBM 7094, под руководством ОС Compatible Time-Sharing System. Большое влияние оказал способ получения машинного кода через интерпретацию, который был использован в имплементации экспериментального языка программирования LC2 компанией Mitchell в 1970 году.

Язык Smalltalk содержал в себе новаторские аспекты JIT-компиляции. Например, трансляция машинного кода выполнялась по необходимости, а результат компиляции кэшировался для дальнейшего использования. В случае нехватки памяти, система удаляла частицы этого кода и восстанавливала новой компиляцией при необходимости. Язык Self, «диалект» языка Smalltalk который был разработан компанией Sun, развил эти техники и некоторое время был самым быстрым из семейства Smalltalk, достигая половины скорости оптимизированного кода на C, будучи полностью объектно-ориентированным языком.

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

JIT-компиляция требует большего внимания к вопросам безопасности и несет повышенные риски, поскольку имеет целью выполнение автогенерированного машинного кода. Скомпилированный код сохраняется в память и сразу выполняется.

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

1. записи кода в память и

2. выставление пометки только для чтения (read-only), поскольку одновременное разрешение на запись и выполнение сегмента памяти является потенциальной опасностью (см. W^X), например, Javascript JIT-компилятор Firefox'а получил такую имплементацию в версии Firefox 46.

JIT spraying является подвидом эксплойта, который использует JIT-компиляцию как элемент heap spraying атаки, что позволяет обойти ASLR и защита исполнительного пространства, заполнив кучу исполнительным кодом.

Вторник, 7 декабря, 2021

Возможно вас заинтересует:

Материал подготовил Леонид Кузнецов