Включает ли компиляция, которая генерирует промежуточный байт-код (как в Java), вместо того, чтобы идти «полностью» до машинного кода, как правило, меньшую сложность (и, следовательно, скорее всего, она занимает меньше времени)?
Включает ли компиляция, которая генерирует промежуточный байт-код (как в Java), вместо того, чтобы идти «полностью» до машинного кода, как правило, меньшую сложность (и, следовательно, скорее всего, она занимает меньше времени)?
Ответы:
Да, компиляция в байт-код Java проще, чем компиляция в машинный код. Это отчасти потому, что существует только один формат для таргетинга (как упоминает Mandrill, хотя это только уменьшает сложность компилятора, а не время компиляции), отчасти потому, что JVM является гораздо более простой машиной и более удобной для программирования, чем реальные процессоры - как это было разработано в В сочетании с языком Java большинство операций Java очень просто отображаются на одну операцию байт-кода. Еще одной очень важной причиной является то, что практически нетоптимизация происходит. Почти все проблемы эффективности оставлены на усмотрение JIT-компилятора (или JVM в целом), поэтому весь средний конец обычных компиляторов исчезает. Он может проходить через AST один раз и генерировать готовые последовательности байт-кода для каждого узла. Существуют некоторые «административные издержки» при создании таблиц методов, постоянных пулов и т. Д., Но это ничто по сравнению со сложностями, скажем, LLVM.
Компилятор - это просто программа, которая берет текстовые файлы, понятные человеку 1, и переводит их в двоичные инструкции для компьютера. Если вы сделаете шаг назад и задумаетесь над своим вопросом с этой теоретической точки зрения, сложность будет примерно такой же. Однако на более практическом уровне компиляторы байт-кода проще.
Какие широкие шаги должны произойти, чтобы скомпилировать программу?
Там только две реальные различия между ними.
В общем, программа с несколькими модулями компиляции требует компоновки при компиляции с машинным кодом и обычно не с байтовым кодом. Можно было бы расстроиться из-за того, является ли связывание частью компиляции в контексте этого вопроса. Если это так, компиляция байтового кода будет немного проще. Однако сложность компоновки компенсируется во время выполнения, когда многие проблемы компоновки обрабатываются виртуальной машиной (см. Мое примечание ниже).
Компиляторы байтового кода имеют тенденцию не оптимизировать так много, потому что виртуальная машина может делать это лучше на лету (компиляторы JIT являются довольно стандартным дополнением к виртуальным машинам в настоящее время).
Из этого я делаю вывод, что компиляторы байтового кода могут опускать сложность большинства оптимизаций и всех ссылок, откладывая оба из них до времени выполнения виртуальной машины. Компиляторы байт-кода на практике проще, поскольку они переносят многие сложности на виртуальную машину, которые компиляторы машинного кода берут на себя.
1 Не считая эзотерических языков
Я бы сказал, что это упрощает конструкцию компилятора, поскольку компиляция - это всегда Java для универсального кода виртуальной машины. Это также означает, что вам нужно только один раз скомпилировать код, и он будет работать на любой платформе (вместо того, чтобы компилировать на каждой машине). Я не уверен, что время компиляции будет меньше, потому что вы можете рассматривать виртуальную машину как стандартную машину.
С другой стороны, на каждой машине должна быть загружена виртуальная машина Java, чтобы она могла интерпретировать «байт-код» (который представляет собой код виртуальной машины, полученный в результате компиляции кода Java), преобразовать его в фактический машинный код и запустить его. ,
Imo это хорошо для очень больших программ, но очень плохо для маленьких (потому что виртуальная машина - пустая трата памяти).
Сложность компиляции во многом зависит от семантического разрыва между исходным языком и целевым языком и уровня оптимизации, который вы хотите применить при преодолении этого разрыва.
Например, компиляция исходного кода 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
указатели,…), которые проще «принудить» делать, что вы хотите, чем примитивы более высокого уровня, такие как вызовы методов или исключения.
Таким образом, можно сказать, что чем выше целевой язык, тем ближе он должен соответствовать семантике исходного языка, чтобы уменьшить сложность компилятора.
На практике большинство современных 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, а затем извлечь выгоду из многих оптимизационных возможностей среднего и заднего плана. конечные части этих компиляторов.