Компиляция в байт-код против машинного кода

13

Включает ли компиляция, которая генерирует промежуточный байт-код (как в Java), вместо того, чтобы идти «полностью» до машинного кода, как правило, меньшую сложность (и, следовательно, скорее всего, она занимает меньше времени)?

Джулиан А.
источник

Ответы:

22

Да, компиляция в байт-код Java проще, чем компиляция в машинный код. Это отчасти потому, что существует только один формат для таргетинга (как упоминает Mandrill, хотя это только уменьшает сложность компилятора, а не время компиляции), отчасти потому, что JVM является гораздо более простой машиной и более удобной для программирования, чем реальные процессоры - как это было разработано в В сочетании с языком Java большинство операций Java очень просто отображаются на одну операцию байт-кода. Еще одной очень важной причиной является то, что практически нетоптимизация происходит. Почти все проблемы эффективности оставлены на усмотрение JIT-компилятора (или JVM в целом), поэтому весь средний конец обычных компиляторов исчезает. Он может проходить через AST один раз и генерировать готовые последовательности байт-кода для каждого узла. Существуют некоторые «административные издержки» при создании таблиц методов, постоянных пулов и т. Д., Но это ничто по сравнению со сложностями, скажем, LLVM.

Роберт Харви
источник
Вы написали "... средний конец ...". Вы имели в виду "... от середины до конца ..."? Или, может быть, "... средняя часть ..."?
Джулиан А.
6
@Julian "middle end" - это реальный термин, придуманный по аналогии с "front end" и "back end" без учета семантики :)
7

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

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

  1. Сканирование, анализ и проверка исходного кода.
  2. Преобразование источника в абстрактное синтаксическое дерево.
  3. Необязательно: обработайте и улучшите AST, если это позволяет спецификация языка (например, удаление мертвого кода, операции переупорядочения, другие оптимизации)
  4. Преобразование AST в какую-то форму, понятную машине.

Там только две реальные различия между ними.

  • В общем, программа с несколькими модулями компиляции требует компоновки при компиляции с машинным кодом и обычно не с байтовым кодом. Можно было бы расстроиться из-за того, является ли связывание частью компиляции в контексте этого вопроса. Если это так, компиляция байтового кода будет немного проще. Однако сложность компоновки компенсируется во время выполнения, когда многие проблемы компоновки обрабатываются виртуальной машиной (см. Мое примечание ниже).

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

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

1 Не считая эзотерических языков


источник
3
Игнорирование оптимизаций и тому подобное глупо. Эти «необязательные шаги» составляют основную часть кода, сложность и время компиляции большинства компиляторов.
На практике это правильно. Я был здесь академическим, я обновил свой ответ.
Есть ли какая-либо языковая спецификация, которая на самом деле запрещает оптимизацию? Я понимаю, что некоторые языки затрудняют, но не позволяют начать с любого?
Davidmh
@Davidmh Я не знаю ни одной спецификации, которая запрещает их. Насколько я понимаю, большинство говорят, что компилятору разрешено, но не будем вдаваться в подробности. Каждая реализация отличается тем, что многие оптимизации основаны на деталях процессора, ОС и целевой архитектуры в целом. По этой причине компилятор байт-кода с меньшей вероятностью оптимизирует и вместо этого направит его на виртуальную машину, которая знает базовую архитектуру.
4

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

С другой стороны, на каждой машине должна быть загружена виртуальная машина Java, чтобы она могла интерпретировать «байт-код» (который представляет собой код виртуальной машины, полученный в результате компиляции кода Java), преобразовать его в фактический машинный код и запустить его. ,

Imo это хорошо для очень больших программ, но очень плохо для маленьких (потому что виртуальная машина - пустая трата памяти).

мандрил
источник
Понимаю. Итак, вы думаете, что сложность сопоставления байт-кода со стандартной машиной (то есть JVM) будет соответствовать сложности сопоставления исходного кода с физической машиной, и не будет никаких оснований полагать, что байт-код приведет к сокращению времени компиляции?
Джулиан А.
Это не то, что я сказал. Я сказал, что сопоставление кода Java с байтовым кодом (который является ассемблером виртуальных машин) будет соответствовать сопоставлению исходного кода (Java) с кодом физической машины.
Мандрил
3

Сложность компиляции во многом зависит от семантического разрыва между исходным языком и целевым языком и уровня оптимизации, который вы хотите применить при преодолении этого разрыва.

Например, компиляция исходного кода Java в байт-код JVM относительно проста, поскольку существует базовое подмножество Java, которое в значительной степени напрямую сопоставляется с подмножеством байтового кода JVM. Есть некоторые различия: в Java есть циклы, но нет GOTO, в JVM есть, GOTOно нет циклов, в Java есть универсальные элементы, в JVM нет, но с ними можно легко справиться (преобразование из циклов в условные переходы тривиально, стирание типов немного меньше так, но все же управляемо). Есть и другие отличия, но менее серьезные.

Компиляция Ruby , исходный код для виртуальной машины Java байт - код намного больше вовлечен (особенно до invokedynamicи MethodHandlesбыли введены в Java 7, а точнее в 3 - м издании спецификации JVM). В Ruby методы могут быть заменены во время выполнения. В JVM наименьшей единицей кода, которую можно заменить во время выполнения, является класс, поэтому методы Ruby должны компилироваться не в методы JVM, а в классы JVM. Диспетчеризация метода Ruby не совпадает с диспетчеризацией метода JVM, и до invokedynamicэтого не было никакого способа внедрить собственный механизм диспетчеризации методов в JVM. В Ruby есть продолжения и сопрограммы, но у JVM нет средств для их реализации. (JVMGOTO ограничен для перехода по целям в методе.) Единственный примитив потока управления, который есть у JVM, достаточно мощный, чтобы реализовывать продолжения, являются исключениями и реализовывать потоки сопрограмм, оба из которых являются чрезвычайно тяжелыми, тогда как цель сопрограмм состоит в том, чтобы быть очень легким.

OTOH, компиляция исходного кода Ruby в байтовый код Rubinius или байтовый код YARV снова тривиальна, так как оба они явно разработаны как цель компиляции для Ruby (хотя Rubinius также использовался для других языков, таких как CoffeeScript и наиболее известный Fancy) ,

Аналогично, компиляция нативного кода x86 в байт-код JVM не так проста, опять же, существует довольно большой семантический пробел.

Еще один хороший пример - Haskell: в Haskell есть несколько высокопроизводительных, готовых к работе промышленных компиляторов, которые производят машинный код x86, но на сегодняшний день не существует работающего компилятора ни для JVM, ни для CLI, потому что семантическая разрыв настолько велик, что преодолеть его очень сложно. Итак, это пример, где компиляция в машинный код на самом деле менее сложна, чем компиляция в байтовый код JVM или CIL. Это связано с тем, что нативный машинный код имеет примитивы более низкого уровня ( GOTOуказатели,…), которые проще «принудить» делать, что вы хотите, чем примитивы более высокого уровня, такие как вызовы методов или исключения.

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

Йорг Миттаг
источник
0

На практике большинство современных 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 в качестве выходных данных, а затем генерируют из него ассемблерный или машинный код) в объектный код.

Кстати, с недавними GCC (в частности, libgccjit ) и инфраструктурой LLVM, вы можете использовать их для компиляции какого-либо другого (или вашего собственного) языка в их внутренние представления Gimple или LLVM, а затем извлечь выгоду из многих оптимизационных возможностей среднего и заднего плана. конечные части этих компиляторов.

Василий Старынкевич
источник