Чи компіляція, яка виробляє проміжний байт-код (як, наприклад, Java), замість того, щоб переходити "на весь шлях" до машинного коду, як правило, передбачає меншу складність (і, таким чином, швидше за все, потребуватиме менше часу)?
Чи компіляція, яка виробляє проміжний байт-код (як, наприклад, Java), замість того, щоб переходити "на весь шлях" до машинного коду, як правило, передбачає меншу складність (і, таким чином, швидше за все, потребуватиме менше часу)?
Відповіді:
Так, компілювати в байт-код Java простіше, ніж компілювати в машинний код. Це частково тому, що для націлювання існує лише один формат (як згадує Mandrill, хоча це лише зменшує складність компілятора, а не час компіляції), почасти тому, що JVM є набагато простішою машиною і зручнішою для програмування, ніж реальні процесори - як це було розроблено в тандем з мовою Java, більшість операцій з Java дуже просто поєднується з операцією байт-коду. Ще одна дуже важлива причина - це те, що практично немаєвідбувається оптимізація. Майже всі проблеми щодо ефективності залишаються на компіляторі JIT (або на JVM в цілому), тому весь середній кінець звичайних компіляторів зникає. В основному він може пройти один раз по AST і генерувати готові послідовності байтових кодів для кожного вузла. Існують певні "адміністративні витрати" генерування таблиць методів, постійних пулів тощо, але це ніщо в порівнянні зі складністю, скажімо, LLVM.
Компілятор - це просто програма, яка приймає прочитані людиною 1 текстові файли і переводить їх у бінарні інструкції для машини. Якщо ви зробите крок назад і подумаєте над своїм питанням з цієї теоретичної точки зору, складність приблизно однакова. Однак на більш практичному рівні компілятори байтових кодів простіші.
Які широкі кроки повинні відбутися для складання програми?
Існує лише дві реальні відмінності між ними.
Загалом, програма з декількома одиницями компіляції вимагає посилання під час компіляції до машинного коду і, як правило, не має байтового коду. Можна розділити волоски щодо того, чи є зв'язок частиною складання у контексті цього питання. Якщо так, компіляція байтового коду буде дещо простішою. Однак складність зв’язування складена під час виконання, коли VM обробляє багато проблем, пов'язаних із зв’язком (див. Мою примітку нижче).
Компілятори байтових кодів, як правило, не оптимізують стільки, тому що VM може зробити це краще на льоту (компілятори JIT - сьогодні досить стандартне доповнення до VM).
З цього випливаю висновок, що компілятори байтових кодів можуть опускати складність більшості оптимізацій та всіх зв'язків, відкладаючи обидва з них на час виконання VM. Компілятори байтових кодів на практиці простіші, оскільки вони переносять багато складностей на VM, які компілятори машинного коду беруть на себе.
1 Не рахуючи езотеричних мов
Я б сказав, що спрощує дизайн компілятора, оскільки компіляція завжди є Java для загального коду віртуальної машини. Це також означає, що вам потрібно зібрати код лише один раз, і він буде працювати на будь-якій платформі (замість того, щоб компілювати на кожній машині). Я не такий впевнений, чи буде час компіляції меншим, оскільки ви можете вважати віртуальну машину так само, як стандартизовану машину.
З іншого боку, для кожної машини доведеться завантажувати віртуальну машину Java, щоб вона могла інтерпретувати "байт-код" (що є кодом віртуальної машини, отриманим у результаті компіляції коду Java), перевести його у фактичний код машини та запустити його .
Imo, це добре для дуже великих програм, але дуже погано для маленьких (адже віртуальна машина - це марно пам'ять).
Складність компіляції багато в чому залежить від семантичного розриву між мовою джерела та цільовою мовою та рівнем оптимізації, яку ви хочете застосувати під час подолання цього розриву.
Наприклад, компіляція вихідного коду Java до байтового коду JVM є відносно прямим, оскільки існує основний підмножина Java, яка в значній мірі відображається безпосередньо до підмножини байтового коду JVM. Є деякі відмінності: у Java є петлі, але ні GOTO
, у JVM немає, GOTO
але немає циклів, у Java є дженерики, з JVM немає, але з ними можна легко впоратися (перетворення з циклів у умовні стрибки тривіальне, тип стирання трохи менше так, але все ж керовано). Є й інші відмінності, але менш серйозні.
Компіляція вихідного коду Ruby до байтового коду JVM значно більше задіяна (особливо раніше invokedynamic
та MethodHandles
була введена в Java 7, а точніше у 3-му виданні специфікації JVM). У Ruby методи можна замінити під час виконання. У JVM найменша одиниця коду, яку можна замінити під час виконання, - це клас, тому методи Ruby повинні бути зібрані не до методів JVM, а до класів JVM. Відправлення методу Ruby не відповідає диспетчеризації методу JVM, і раніше invokedynamic
не було можливості ввести свій власний механізм диспетчеризації методу в JVM. У Рубі є продовження та розробки, але СП не вистачає можливостей для їх здійснення. (Спілки JVMGOTO
обмежений для стрибків цілей в рамках методу.) Єдиний примітивний потік управління, який має JVM, який буде достатньо потужним для здійснення продовження, - це винятки та реалізація потоків корутин, обидві з яких мають надзвичайно важку вагу, тоді як вся мета супротивників полягає в тому, щоб бути дуже легким.
OTOH, компіляція вихідного коду Ruby до байтового коду Rubinius або байтового коду YARV знову тривіальна, оскільки обидва вони явно розроблені як ціль компіляції для Ruby (хоча Rubinius також використовувався для інших мов, таких як CoffeeScript, і найвідоміше Fancy) .
Так само, компіляція нативного коду x86 до байтового коду JVM не є прямолінійним, знову ж таки, існує досить великий семантичний пробіл.
Haskell - ще один хороший приклад: у Haskell є кілька високопродуктивних компіляторів, готових до промислової міцності, які виробляють нативний машинний код x86, але на сьогоднішній день не існує робочого компілятора ні для JVM, ні для CLI, оскільки семантичний розрив настільки великий, що його дуже складно подолати. Отже, це приклад, коли компіляція до нативного машинного коду насправді менш складна, ніж компіляція у байт-код JVM або CIL. Це тому, що в кодовому машинному коді є набагато нижчі рівні примітивів ( GOTO
, покажчики, ...), які можна легше "примусити" робити те, що ви хочете, ніж використовувати примітиви вищого рівня, такі як виклики методів або винятки.
Отже, можна сказати, що чим вище рівень цільової мови, тим більше вона повинна відповідати семантиці мови-джерела, щоб зменшити складність компілятора.
На практиці більшість JVM сьогодні є дуже складним програмним забезпеченням, що робить компіляцію JIT (тому байт-код динамічно переводиться на машинний код JVM).
Отже, хоча компіляція з вихідного коду Java (або вихідного коду Clojure) до байтового коду JVM дійсно простіша, сам JVM робить складний переклад на машинний код.
Той факт, що цей JIT-переклад всередині JVM є динамічним, дозволяє JVM зосередитись на найбільш релевантних частинах байтового коду. Практично кажучи, більшість JVM оптимізують більш гарячі частини (наприклад, найбільш викликані методи або найбільш виконані базові блоки) байт-коду JVM.
Я не впевнений, що комбінована складність JVM + Java для компілятора байт-кодів значно менша, ніж складність дострокових компіляторів.
Зауважте також, що більшість традиційних компіляторів (наприклад, GCC або Clang / LLVM ) перетворюють вихідний код C (або C ++, або Ada, ...) у внутрішнє представлення ( Gimple для GCC, LLVM для Clang), яке досить схоже на деякий байт-код. Потім вони перетворюють, що внутрішні представлення (спочатку оптимізуючи його в себе, тобто більшість пропусків оптимізації GCC приймають Gimple як вхід і виробляють Gimple як вихід; пізніше випромінюють з нього асемблер або машинний код) в об'єктний код.
BTW, з недавньою GCC (особливо libgccjit ) та інфраструктурою LLVM, ви можете використовувати їх для компіляції якоїсь іншої (або вашої власної) мови у їх внутрішні представлення Gimple або LLVM, а потім отримувати прибуток від багатьох можливостей оптимізації середнього та заднього класів кінцеві частини цих укладачів.