.NET Internals 09 Just-In-Time (JIT) compilation

.NET Internals 09 Just-In-Time (JIT) compilation


Дмитрий Бахтенков

Вы когда - нибудь слышали термин "JIT"? Это аббревиатура от "Just-in-time". JIT-компилятор - это инструмент, который выполняет JIT-компиляцию, что является важной особенностью приложений .NET. Давайте сегодня рассмотрим эту тему.

Содержание:

  • Что делает управляемые приложения переносимыми?
  • Intermediate Language (IL)

- Просмотр MSIL

  • Just-In-Time (JIT) - компиляция

- Как Toyota принесла нам JIT?

- JIT компилятор

- Можем ли мы все еще компилировать код заранее?

Что делает управляемые приложения переносимыми?

Как мы знаем из предыдущих статей из этой серии, .NET Framework и CLR предоставляют множество полезных функций для приложений, ориентированных на конкретную платформу, таких как автоматическое управление памятью. Однако одной из главных целей изобретения управляемых сред выполнения было сделать реализованные приложения переносимыми. Так что же означает эта переносимость? Это означает, что, во-первых, он может быть запущен на любом оборудовании. В идеале он также должен быть независимым программным обеспечением (особенно независимым от операционной системы). Мы все еще можем наблюдать эту тенденцию, например, по тому факту, что Microsoft создала и активно разрабатывает NET Core. Такая переносимость не только делает возможным запуск приложения на любой аппаратной или программной платформе, но и освобождает разработчиков от необходимости заботиться о базовых низкоуровневых структурах. Например, при работе с TPL (Task Parallel Library) программисту обычно не нужно изменять свой код с учетом базового оборудования (например, количества или архитектуры процессоров). То же самое касается распределения памяти (описано в предыдущих статьях), где многие детали, близкие к hardware, отличаются в зависимости от архитектуры операционной системы (32/64 бит) - CLR обрабатывает это для нас.

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

Intermediate Language (IL)

По причинам, описанным выше, исходный код языков программирования управляемых сред выполнения (например, C#, F# или Java) напрямую не компилируется на язык ассемблера. Вместо этого он сначала компилируется на промежуточный язык (IL). Промежуточный язык CLR также называется MSIL (Microsoft Intermediate Language).

Компиляция исходного кода в IL выполняется компилятором конкретного языка. Это процесс, который происходит, когда вы создаете свое приложение, нажимая F5 в Visual Studio или используя csc.exe для компиляции вашего кода.

Исходный код -> Компиляция IL, которая выполняется компилятором конкретного языка. Например, код на C# компилируется Roslyn. который является компилятором языка C# :

Visual Studio’s build – Roslyn


Просмотр MSIL

Чтобы просмотреть код MSIL, содержащийся в скомпилированных файлах EXE/DLL (как я показывал я вам, например, здесь и здесь), вы можете использовать расширение ILSpy Visual Studio, которое при установке добавляет опцию меню в Visual Studio (Инструменты -> ILSpy), где вы можете открыть любой скомпилированный файл и просмотреть код IL содержащихся объектов:

ILSpy – MSIL code

Что на самом деле скомпилировано в код IL? На данный момент мы можем сказать, что наиболее важными являются методы (сгруппированные в классы, пространства имен и т.д., Конечно). Есть также много других вещей (даже больше, чем вы могли бы ожидать, посмотрев на свой исходный код), но сейчас мы сосредоточимся на методах.

Хорошо, теперь у нас есть код IL, но он пока не может быть понят процессором. Как и когда он затем компилируется в ассемблерный код?

Just-In-Time (JIT) компиляция

Как Toyota принесла нам JIT?

Давайте начнем с некоторой исторической предыстории 🙂 В начале 1960 – х годов японским инженерам Toyota пришлось реорганизовать управление складом, потому что у них были очень высокие затраты на хранение - доставка деталей от поставщиков занимала много времени, потому что они заказывали много всего заранее. Каждой доставкой должен был кто - то заниматься, поэтому им требовалось много сотрудников. Заказанные запчасти долго хранились на складах, требовали много места для хранения и технического обслуживания. Чтобы минимизировать затраты, они изобрели just-in-time производство (также известное как производственная система Toyota, Toyota Production System). Его основным принципом было заказывать товары у поставщиков только тогда, когда был достигнут минимальный уровень запасов на складе. По сути, товары были доставлены как раз вовремя, когда они были необходимы на производственной линии. Это минимизировало количество поставок (и сотрудников склада) и в то же время не требовало столько места для хранения, как раньше. Всю идею можно представить в следующем порядке потоков:

Toyota Production System (JIT)

JIT-компилятор

После успеха Toyota 😀 создатели CLR внедрили в платформу JIT-компилятор (just-in-time). Его роль заключается в компиляции кода промежуточного языка на язык ассемблера в соответствии с характеристиками аппаратного обеспечения и операционной системы (в зависимости от машины, на которой выполняется JIT-компиляция кода). В отличие от неуправляемых языков, в которых исходный код компилируется на машинный язык до выполнения программы, IL (по умолчанию) компилируется в инструкции процессора во время выполнения. Вот как Инженеры NET сделали так, чтобы машинный код был вовремя доставлен в центральный процессор.

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

Своевременная компиляция на самом деле представляет собой комбинацию двух подходов: заблаговременной компиляции и интерпретации, сочетающих преимущества и недостатки обоих. Однако компиляция кода IL в сборку один во время выполнения позволяет использовать несколько интересных функций, таких как динамическая типизация (возможность получения фактического типа объекта при выполнении программы). Вот почему JIT-компиляция часто рассматривается как форма динамической компиляции.

Еще одним преимуществом JIT-компиляции является то, что исходный код, который мы пишем, находится “дальше от аппаратного обеспечения”, поэтому его можно сделать более читаемым и удобным для человека, в то время как материал, касающийся hardware, “скрыт” в IL (слава разработчикам MSIL!)

JIT – компиляция также имеет некоторые затраты - естественно, для компиляции кода во время выполнения требуется время. С другой стороны, различные процессорные блоки могут иметь разные мощные наборы инструкций или процессоры, которые могут использоваться в сборке, создаваемой JIT, чего не было бы при предварительной компиляции (без какой-либо специальной настройки).

Можем ли мы все еще компилировать код заранее?

Если вам действительно не нравится JIT (зачем мне это?), есть способы предварительной компиляции EXE/DLL перед выполнением приложения. Это называется предварительной компиляцией JIT. Одной из причин этого может быть то, что многие пользователи будут запускать наше приложение из одних и тех же файлов EXE/DLL, что обычно приводит к JIT-компиляции IL для каждого пользователя отдельно. В таком случае может потребоваться предварительная JIT-компиляция файлов нашего приложения по соображениям производительности и использования памяти. Это можно сделать с помощью NGen (для всех версий .NET) или .NET Native (для .NET-приложений, ориентированные на Windows 10). Сегодня мы не будем подробно описывать эти инструменты (более подробную информацию вы можете найти в Интернете).

Report Page