Кажется, здесь есть как минимум два разных возможных вопроса. Один действительно о компиляторах вообще, с Java в основном только пример жанра. Другой более специфичен для Java - конкретные байтовые коды, которые он использует.
Компиляторы в целом
Давайте сначала рассмотрим общий вопрос: почему компилятор использует некоторое промежуточное представление в процессе компиляции исходного кода для запуска на каком-то конкретном процессоре?
Снижение сложности
Один из ответов на этот вопрос довольно прост: он преобразует задачу O (N * M) в задачу O (N + M).
Если нам дано N исходных языков и M целей, и каждый компилятор полностью независим, то нам нужно N * M компиляторов для преобразования всех этих исходных языков во все эти цели (где «target» - это что-то вроде комбинации процессор и ОС).
Однако, если все эти компиляторы согласовывают общее промежуточное представление, тогда мы можем иметь N внешних интерфейсов компилятора, которые переводят исходные языки в промежуточное представление, и M внутренних частей компилятора, которые переводят промежуточное представление во что-то подходящее для конкретной цели.
Проблема сегментации
Более того, он разделяет проблему на два более или менее эксклюзивных домена. Люди, которые знают / заботятся о дизайне языка, разборе и подобных вещах, могут сосредоточиться на внешних интерфейсах компилятора, в то время как люди, которые знают о наборах команд, дизайне процессора и подобных вещах, могут сосредоточиться на серверной части.
Так, например, учитывая что-то вроде LLVM, у нас есть много внешних интерфейсов для разных языков. У нас также есть бэк-энды для множества разных процессоров. Специалист по языку может написать новый интерфейс для своего языка и быстро поддержать множество целей. Парень из процессора может написать новый бэкэнд для своей цели, не занимаясь языковым дизайном, анализом и т. Д.
Разделение компиляторов на внешний и внутренний интерфейсы с промежуточным представлением для взаимодействия между ними не является оригинальным в Java. Долгое время это было довольно распространенной практикой (во всяком случае, задолго до появления Java).
Модели распространения
В той мере, в которой Java добавил что-то новое в этом отношении, это было в модели распространения. В частности, даже если компиляторы были разделены на внутренние и внутренние части в течение длительного времени, они, как правило, распространялись как один продукт. Например, если вы купили компилятор Microsoft C, внутри он имел «C1» и «C2», которые были интерфейсом и бэкэндом соответственно - но вы купили только «Microsoft C», который включал оба части (с "драйвером компилятора", который координировал операции между ними). Несмотря на то, что компилятор был построен из двух частей, для обычного разработчика, использующего компилятор, это была всего лишь одна вещь, которая переводилась из исходного кода в объектный код, и между ними ничего не было видно.
Вместо этого Java распространяла интерфейс в Java Development Kit, а интерфейс в виртуальной машине Java. У каждого пользователя Java был серверный компилятор, предназначенный для любой системы, которую он использовал. Разработчики Java распространяли код в промежуточном формате, поэтому, когда пользователь загружал его, JVM делала все необходимое для его выполнения на своей конкретной машине.
Прецеденты
Обратите внимание, что эта модель распределения не была полностью новой. Например, P-система UCSD работала аналогично: внешние интерфейсы компилятора создавали P-код, и каждая копия P-системы включала виртуальную машину, которая выполняла то, что было необходимо для выполнения P-кода на этой конкретной цели 1 .
Java-байт-код
Java-байт-код очень похож на P-код. Это в основном инструкции для довольно простой машины. Предполагается, что эта машина является абстракцией существующих машин, поэтому ее довольно легко быстро перевести практически к любой конкретной цели. Простота перевода была важна на раннем этапе, потому что первоначальное намерение состояло в том, чтобы интерпретировать байтовые коды, как это делала P-System (и, да, именно так работали ранние реализации).
Сильные стороны
Java-байт-код легко создать для внешнего интерфейса компилятора. Если (например) у вас есть довольно типичное дерево, представляющее выражение, обычно довольно легко пройти по дереву и сгенерировать код достаточно непосредственно из того, что вы найдете в каждом узле.
Байт-коды Java довольно компактны - в большинстве случаев гораздо более компактны, чем исходный код или машинный код для большинства типичных процессоров (и, особенно для большинства процессоров RISC, таких как SPARC, продаваемый Sun при разработке Java). Это было особенно важно в то время, потому что одной из основных целей Java была поддержка апплетов - кода, встроенного в веб-страницы, который должен быть загружен перед выполнением - в то время, когда большинство людей обращалось к нам через модемы по телефонным линиям около 28,8. килобит в секунду (хотя, конечно, было еще немало людей, использующих более старые, более медленные модемы).
Слабые стороны
Основным недостатком байт-кодов Java является то, что они не особенно выразительны. Хотя они могут достаточно хорошо выражать концепции, представленные в Java, они не так хорошо работают для выражения концепций, не являющихся частью Java. Точно так же, хотя на большинстве машин легко выполнять байт-коды, гораздо сложнее сделать это таким образом, чтобы в полной мере использовать преимущества любой конкретной машины.
Например, довольно обычным делом является то, что если вы действительно хотите оптимизировать байтовые коды Java, вы в основном выполняете реверс-инжиниринг, чтобы перевести их обратно из представления, подобного машинному коду, и превратить их обратно в инструкции SSA (или что-то подобное) 2 . Затем вы манипулируете инструкциями SSA, чтобы выполнить оптимизацию, а затем переводите что-то, что соответствует архитектуре, которая вас действительно интересует. Однако даже с этим довольно сложным процессом некоторые концепции, которые чужды Java, достаточно сложно выразить, что трудно перевести из некоторых исходных языков в машинный код, который оптимально работает (даже близко) на большинстве типичных машин.
Резюме
Если вы спрашиваете, зачем вообще использовать промежуточные представления, то есть два основных фактора:
- Свести задачу O (N * M) к задаче O (N + M) и
- Разбейте проблему на более управляемые части.
Если вы спрашиваете об особенностях байт-кодов Java и о том, почему они выбрали именно это представление вместо какого-то другого, то я бы сказал, что ответ в значительной степени возвращается к их первоначальному замыслу и ограничениям сети в то время. , что приводит к следующим приоритетам:
- Компактное представление.
- Быстро и легко декодировать и выполнять.
- Быстро и легко внедряется на большинстве распространенных машин.
Возможность представлять много языков или оптимально выполнять самые разные задачи была гораздо более низкими приоритетами (если они вообще считались приоритетами).
- Так почему же P-система в основном забыта? В основном ценовая ситуация. P-система продавалась довольно прилично на Apple II, Commodore SuperPets и т. Д. Когда вышел IBM PC, P-система была поддерживаемой ОС, но MS-DOS стоила дешевле (с точки зрения большинства людей, по сути, была добавлена бесплатно) и быстро стало доступно больше программ, поскольку именно для этого писали Microsoft и IBM (среди прочих).
- Например, так работает Сажа .