Розуміння відмінностей: традиційний перекладач, компілятор JIT, перекладач JIT та компілятор AOT


130

Я намагаюся зрозуміти відмінності між традиційним перекладачем, компілятором JIT, перекладачем JIT та компілятором AOT.

Перекладач - це лише машина (віртуальна чи фізична), яка виконує вказівки на якійсь комп'ютерній мові. У цьому сенсі JVM є перекладачем, а фізичні процесори - інтерпретаторами.

Попередня компіляція просто означає компілювати код до якоїсь мови перед її виконанням (інтерпретацією).

Однак я не впевнений у точних визначеннях компілятора JIT та інтерпретатора JIT.

Згідно з означенням, яке я прочитав, компіляція JIT - це просто складання коду безпосередньо перед його інтерпретацією.

Отже, компіляція JIT - це компіляція AOT, зроблена безпосередньо перед виконанням (інтерпретація)?

І інтерпретатор JIT - це програма, яка містить і компілятор JIT, і інтерпретатор, і збирає код (JITs це) безпосередньо перед тим, як його інтерпретувати?

Будь ласка, уточнюйте відмінності.


4
Чому ви змусили вас повірити, що існує різниця між "компілятором JIT" та "перекладачем JIT"? Вони по суті є двома різними словами для однієї і тієї ж речі. Загальна концепція JIT однакова, але існує широкий спектр методів реалізації, які неможливо просто розділити на "компілятор" проти "інтерпретатор".
Грег Х'югілл

2
Читайте wikipages на Just In Time компіляції , AOT компілятор , компілятора , інтерпретатора , байткод , а також книги Queinnec в Ліспі на дрібні шматочки
Basile Starynkevitch

Відповіді:


198

Огляд

Інтерпретатор для мови X являє собою програму (або машину, або просто якийсь - то механізм в цілому) , який виконує будь-яку програму р , написаний на мові X таким чином, що він виконує ефекти і оцінює результати , як це наказано специфікацією X . Процесорні процесори зазвичай є перекладачами відповідних наборів інструкцій, хоча сучасні високопродуктивні процесори на робочих станціях насправді є складнішими за них; Насправді вони можуть мати базовий приватний набір інструкцій і або перекладати (компілювати), або інтерпретувати зовнішній публічний набір інструкцій.

Компілятор від X до Y являє собою програму (або машина, або просто якийсь - то механізм в цілому) , який переводить будь-яку програму р з деякого мови X в семантично еквівалентної програми р ' на деякій мові Y таким чином , що семантика програми зберігаються, тобто , що інтерпретація р ' з перекладачем Y дасть ті ж результати , і мають ті ж ефекти , як інтерпретувати р з перекладачем X . (Зверніть увагу, що X і Y можуть бути однаковою мовою.)

Терміни " Попередній час" (AOT) та " Час у часі" (JIT) відносяться до того, коли відбувається компіляція: "час", зазначений у цих термінах, є "час виконання", тобто компілятор JIT складає програму такою, якою вона є. працює , компілятор AOT збирає програму ще до її запуску . Зауважте, що це вимагає, щоб компілятор JIT з мови X на мову Y повинен якось працювати разом з перекладачем для мови Y, інакше не було б ніякого способу запуску програми. (Так, наприклад, компілятор JIT, що компілює JavaScript до машинного коду x86, не має сенсу без процесора x86; він компілює програму під час її запуску, але без CPU x86 програма не працює.)

Зауважте, що це розрізнення не має сенсу для перекладачів: інтерпретатор запускає програму. Ідея інтерпретатора AOT, який запускає програму до її запуску, або інтерпретатора JIT, який запускає програму під час її запуску, є безглуздим.

Отже, у нас є:

  • AOT компілятор: компілює перед запуском
  • Компілятор JIT: компілюється під час запуску
  • перекладач: працює

Компілятори JIT

У сімействі компіляторів JIT існує ще багато відмінностей щодо того, коли саме вони складаються, як часто і на якій детальності.

Наприклад, компілятор JIT в CLR Майкрософт компілює код лише один раз (коли він завантажений) і компілює цілу збірку за один раз. Інші компілятори можуть збирати інформацію під час роботи програми та перекомпілювати код кілька разів, коли з’являється нова інформація, що дозволяє їм краще оптимізувати її. Деякі компілятори JIT навіть здатні деоптимізувати код. Тепер ви можете запитати себе, чому хто-небудь хотів би це зробити? Деоптимізація дозволяє проводити дуже агресивні оптимізації, які насправді можуть бути небезпечними: якщо виявиться, що ви були занадто агресивними, ви можете знову відмовитися, тоді як при компіляторі JIT, який не вдається знешкодити, ви не змогли запустити агресивні оптимізації в першу чергу.

Компілятори JIT можуть або скомпілювати якусь статичну одиницю коду за один раз (один модуль, один клас, одна функція, один метод,…; зазвичай вони називаються методом JIT, наприклад), або вони можуть простежити динаміку виконання коду для пошуку динамічних слідів (як правило, циклів), які вони потім будуть компілювати (вони називаються відстежуючими JIT).

Поєднання перекладачів та укладачів

Інтерпретатори та компілятори можуть бути об'єднані в єдиний механізм виконання мов. Є два типові сценарії, коли це робиться.

Об'єднуючи AOT компілятор з X в Y з перекладачем Y . Тут, як правило, X - мова вищого рівня, оптимізована для читання людиною, тоді як Yце компактна мова (часто якийсь байт-код), оптимізована для інтерпретації машинами. Наприклад, двигун виконання CPython Python має компілятор AOT, який компілює вихідний код Python в байт-код CPython, і інтерпретатор, що інтерпретує байт-код CPython. Аналогічно, двигун виконання YARV Ruby має компілятор AOT, який компілює вихідний код Ruby в байт-код YARV, і інтерпретатор, що інтерпретує байт-код YARV. Чому б ти хотів це зробити? Ruby та Python - це дуже високий рівень та дещо складні мови, тому ми спочатку компілюємо їх у мову, яку легше розбирати та простіше інтерпретувати, а потім інтерпретувати цю мову.

Інший спосіб поєднання інтерпретатора та компілятора - це механізм виконання змішаного режиму . Тут ми «змішувати» два «режиму» від реалізації такої ж мова разом, тобто інтерпретатор X і JIT компілятор з X в Y . (Отже, різниця тут полягає в тому, що у вищенаведеному випадку ми мали кілька "етапів" з компілятором, який компілював програму, а потім подавали результат в інтерпретатор, тут у нас є два робочих сторони на одній мові. ) Код, складений компілятором, як правило, працює швидше, ніж код, який виконується інтерпретатором, але насправді для складання коду спочатку потрібен час (і, особливо, якщо ви хочете сильно оптимізувати код для запускудійсно швидко, це займає багато часу). Отже, щоб перенести цей час, коли компілятор JIT зайнятий компіляцією коду, інтерпретатор вже може почати запуск коду, і як тільки JIT закінчиться компіляцією, ми можемо переключити виконання на компільований код. Це означає, що ми отримуємо як найкращу ефективність компільованого коду, але нам не потрібно чекати, коли компіляція закінчиться, і наша програма почне працювати відразу (хоча не так швидко, як це могло б бути).

Це насправді лише найпростіше можливе застосування двигуна виконання змішаного режиму. Більш цікаві можливості, наприклад, не починати збирання відразу, а дозволити перекладачеві трохи запустити та зібрати статистику, інформацію про профілювання, ввести інформацію, інформацію про ймовірність того, які конкретні умовні гілки взяті, які методи називаються найчастіше тощо, а потім подавати цю динамічну інформацію до компілятора, щоб вона могла генерувати більш оптимізований код. Це також спосіб здійснити деоптимізацію, про яку я говорив вище: якщо виявиться, що ви були занадто агресивними в оптимізації, ви можете викинути (частину) коду і повернутися до інтерпретації. Наприклад, JVM HotSpot робить це. Він містить як інтерпретатор байт-коду JVM, так і компілятор для байт-коду JVM. (Фактично,два компілятори!)

Також можливо , і справді , загальною для об'єднати ці два підходи: дві фази з першим будучи АОТ компілятор , який компілює X в Y , а друга фаза будучи змішаним режимом роботи двигуна , що і інтерпретує Y і компілює Y в Z . Наприклад, механізм виконання Rubinius Ruby працює таким чином, наприклад: у нього є компілятор AOT, який компілює вихідний код Ruby до байт-коду Rubinius, і движок змішаного режиму, який спочатку інтерпретує байт-код Rubinius і після того, як він зібрав деяку інформацію, компілює найчастіше названі методи в рідну мову машинний код.

Зауважимо, що роль, яку перекладач відіграє у випадку механізму виконання змішаного режиму, а саме забезпечення швидкого запуску, а також потенційно збирання інформації та надання резервної можливості, може також відігравати другий компілятор JIT. Так працює, наприклад, V8. V8 ніколи не інтерпретує, він завжди компілює. Перший компілятор - це дуже швидкий, дуже тонкий компілятор, який запускається дуже швидко. Код, який він створює, не дуже швидкий. Цей компілятор також вводить код профілювання в код, який він створює. Інший компілятор повільніше і використовує більше пам'яті, але створює набагато швидший код, і він може використовувати інформацію про профілювання, зібрану за допомогою запуску коду, зібраного першим компілятором.


1
Чи дійсно компілятори байт-кодів Python та Ruby вважаються AOT? З огляду на те, що обидві мови дозволяють динамічно завантажувати модулі, які компілюються під час завантаження, вони запускаються під час виконання програми.
Себастьян Редл

1
@SebastianRedl, за допомогою CPython ви можете запускати python -m compileall .або завантажувати модулі один раз. Навіть у останньому випадку, оскільки файли залишаються та повторно використовуються після першого запуску, це здається AOT.
Пол Дрейпер

Чи є у вас посилання для подальшого читання? Я хотів би дізнатися більше про V8.
Вінс Пануччо

@VincePanuccio Посилання посилаються на компілятори Full-Codegen і Crankshaft, які з тих пір були замінені . Про них можна знайти в Інтернеті.
eush77

CLR Jit збирає метод за методом, а не всю збірку
Григорій
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.