Почему интерпретаторы компилируют код каждый раз при запуске программы?

Мой вопрос касается всех интерпретируемых языков, но чтобы лучше проиллюстрировать свою точку зрения, я буду использовать Java в качестве примера.

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

Однако все это не имеет для меня никакого смысла. Почему бы не загрузить байт-коды Java на мою машину, чтобы JVM скомпилировала их для моей конкретной архитектуры один раз и создала исполняемый файл, а затем в следующий раз, когда я хочу запустить программу, я просто запускаю скомпилированный исполняемый файл. Таким образом, обещание Java: «написать один раз, запускать везде» по-прежнему выполняется, но без большей части медлительности интерпретируемых языков.

Я знаю, что при компиляции JVM выполняет некоторые умные динамические оптимизации; однако разве их цель не состоит только в том, чтобы компенсировать медлительность механизма интерпретации? Я имею в виду, что если JVM должна компилироваться один раз, а затем запускаться несколько раз, то не перевешивает ли это ускорение оптимизации, выполняемой JVM?

Я полагаю, что здесь есть что-то очевидное, чего мне не хватает. У кого-нибудь есть объяснение?

Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
2
0
403
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Ответ принят как подходящий

Это неправильно:

The JVM is then just a program that takes the java byte codes and compiles them (for the specific architecture) every time I run my program.

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

Чтобы ответить на ваш вопрос, почему бы не сохранить этот скомпилированный результат, есть несколько вопросов:

  • Потребовалось бы место для его хранения.
  • Ему нужно было бы обрабатывать объединение скомпилированных частей с частями, которые не стоило компилировать (или компилировать все это целиком).
  • Потребуется способ надежный узнать, был ли кешированный скомпилированный код актуальным.
  • Оптимизированный, скомпилированный код для программы, запускаемой с аргументами A, B и C, может быть неоптимальным, если программа запускается с аргументами X, Y и Z. компиляция или запоминание аргументов (что было бы несовершенным: например, если бы это были имена файлов или URL-адреса, их содержимое не сохранится) и т. д.
  • Это в значительной степени не нужно, компиляция не занимает много времени. Компиляция байт-кода в машинный код занимает не так много времени, как компиляция исходного кода в машинный код.

Поэтому я думаю, что ответ таков: это сложно и чревато ошибками, поэтому затраты не стоят выгоды.

Оптимизированный код заточен не только под конкретные аргументы программы, но и под текущую среду, которая может (вернее, изменится) измениться даже во время одного запуска. Таким образом, JVM будет деоптимизировать код при изменении окружающих условий, чтобы создать новый оптимизированный код, адаптированный к новым условиям. И эти условия редко совпадают со средой следующего запуска программы.

Holger 18.03.2019 14:02

Любое такое утверждение о том, что делают «интерпретаторы», подлежит наблюдению, что не все интерпретаторы одинаковы.

Например, интерпретатор Python берет исходные файлы .py и запускает их. По пути он генерирует «скомпилированные» файлы .pyc. В следующий раз, когда вы запустите те же файлы .py, шаг «компиляции» можно пропустить, если файлы .py не были изменены. (Я говорю «компиляция» в кавычках, поскольку, насколько мне известно, результат не является машинным кодом).

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

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

Что касается компромисса между компиляцией и интерпретацией: одним из факторов является то, как долго выполняется ваша программа и сколько времени пройдет, прежде чем она изменится. Если вы запускаете короткие «студенческие» программы, которые, скорее всего, выполняются только один раз, прежде чем будут изменены, нет особого смысла прилагать много усилий для компиляции. С другой стороны, если ваша программа управляет устройством, которое, скорее всего, будет включено в течение нескольких недель, JIT-компиляция в устройстве стоит того, чтобы выполнить ее снова, если устройство будет перезагружено.

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

Why Do Interpretors Compile the Code Everytime a Program is Run?

Они не делают. Компилируется интерпретатор никогда. Это интерпретирует. Если бы он скомпилировался, это был бы компилятор, а не интерпретатор.

Интерпретаторы интерпретируют, компиляторы компилируют.

My question is about all interpreted languages, but to illustrate my point better I will use Java as an example.

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

Язык Каждый может быть реализован либо интерпретатором, либо компилятором. Подавляющее большинство языков имеют по крайней мере одну реализацию каждого типа. (Например, существуют интерпретаторы для C и C++, а также компиляторы для JavaScript, PHP, Perl, Python и Ruby.) Кроме того, большинство современных языковых реализаций фактически сочетают в себе интерпретатор и компилятор (или даже несколько компиляторов).

Язык — это всего лишь набор абстрактных математических правил. Интерпретатор — это одна из нескольких конкретных стратегий реализации языка. Эти двое живут на совершенно разных уровнях абстракции. Если бы английский был типизированным языком, термин «интерпретируемый язык» был бы ошибкой типа. Утверждение «Python — это интерпретируемый язык» не просто ложно (поскольку ложность подразумевает, что утверждение даже имеет смысл, даже если оно неверно), оно просто не делает смысл, потому что язык может быть определен как никогда как «интерпретируется».

What I know for Java is that when programmers write their code they have to compile it in java byte codes which are like machine language for a universal java virtual machine architecture. Then they can distribute their code to any machine which runs the Java Virtual Machine (JVM).

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

Кроме того, мне любопытно: в этом абзаце вы описываете Java как язык, который всегда компилируется, а в предыдущем абзаце вы используете Java как пример интерпретируемого языка. Это не имеет смысла.

The JVM is then just a program that takes the java byte codes and compiles them (for the specific architecture) every time I run my program.

Опять же, в Спецификации виртуальной машины Java вообще ничего не говорится о компиляции или интерпретации, не говоря уже о том, когда и как часто компилируется код.

Интерпретация байт-кода JVML совершенно легальна и соответствует спецификации, также вполне совместима и его однократная компиляция, и на самом деле и то, и другое было сделано.

From my understanding (please correct me if I am wrong here) if I run my code the JVM will compile it on the fly, my machine will run the compiled instructions, and when I close the program all the compilation work will be lost, only to be done again, the second time I want to run my program.

Это полностью зависит от того, какую JVM вы используете, какую версию JVM вы используете, а иногда даже от конкретной среды и/или параметров командной строки.

Некоторые JVM интерпретируют байт-код (например, старые версии Sun JVM). Некоторые версии компилируют байт-код однажды (например, Excelsior.JET). Некоторые версии сначала интерпретируют байт-код, собирают профилирующую информацию и статистику во время работы программы, используют эти данные для поиска так называемых «горячих точек» (то есть кода, который выполняется чаще всего и, следовательно, кода, который приносит наибольшую пользу). от ускорения), а затем компилирует эти горячие точки, используя данные профилирования для оптимизации (например, IBM J9, Oracle HotSpot). Некоторые используют аналогичный прием, но вместо интерпретатора используют неоптимизирующий быстрый компилятор. Некоторые кэшируют и повторно используют скомпилированный собственный машинный код (например, ныне заброшенный JRockit).

This is also the reason why generally interpreted languages are slow, because they have to compile every time on the fly.

Нет смысла говорить о медленном или быстром языке. Языки не бывают медленными или быстрыми. Язык — это всего лишь лист бумаги.

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

В общем, производительность в основном зависит от денег и в меньшей степени от среды исполнения. Запуск определенного фрагмента кода, написанного на C++, скомпилированного с помощью Microsoft Visual C++, в Windows на MacPro, скорее всего, будет быстрее, чем запуск аналогичного фрагмента кода, написанного на Ruby, выполненного YARV в Windows на MacPro.

Однако основная причина этого заключается в том, что Microsoft — гигантская компания, вложившая в Visual C++ огромные суммы денег, исследований, инженерных разработок, рабочей силы и других ресурсов, в то время как YARV в основном работает на добровольных началах. Кроме того, оптимизированы большинство основных операционных систем, таких как Windows, macOS, Linux, различные BSD и Unices и т. д., а также наиболее распространенные архитектуры ЦП, такие как AMD64, x86, PowerPC, ARM, SPARC, MIPS, Super-H и т. д. для ускорения программ на C-подобных языках и имеет гораздо меньше оптимизаций для языков, подобных Smalltalk. На самом деле, некоторые функции даже активно причинить боль их (например, виртуальная память может значительно увеличить задержки сборки мусора, даже если она совершенно бесполезна в языке с управлением памятью).

However, all of this makes no sense for me. Why not download the java byte codes on my machine, have the JVM compile them for my specific architecture once and create an executable file, and then the next time I want to run the program, I just run the compiled executable file.

Если вы этого хотите, никто вас не остановит. Например, это именно то, что делает Excelsior.JET. Никто не заставляет вас использовать IBM J9 или Oracle HotSpot.

I know that while compiling the JVM does some clever dynamic optimizations; however isn't their purpose only to compensate for the slowness of the interpretation mechanism?

Эти динамические оптимизации Только возможны именно потому что, они динамические. Есть несколько фундаментальных невозможных результатов в программировании, которые серьезно ограничивают виды оптимизации, которые может выполнять статический опережающий компилятор. Проблема остановки, теорема Райса, проблема функции и т. д.

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

Динамический JIT-компилятор, который компилирует код во время выполнения, не должен доказывать, что метод не переопределен. Не нужно статически вычислять, какой будет иерархия классов во время выполнения. Иерархия классов тут как тут: может просто Смотреть: метод переопределен или нет? Следовательно, динамический компилятор может встраиваться в гораздо большем количестве случаев, чем статический компилятор.

Но есть еще кое-что. Динамический компилятор также может выполнять деоптимизация. Теперь вы можете задаться вопросом: зачем вам де-оптимизировать? Зачем делать код хуже? Ну вот почему: если вы знаете, что можете деоптимизировать, то можете делать оптимизации на основе догадки, а когда окажется, что угадали, то можно просто снова убрать оптимизацию.

Продолжая наш пример со встраиванием: в отличие от статического компилятора, наш динамический компилятор может определить со 100% точностью, переопределен метод или нет. Однако он не обязательно может знать, будет ли переопределенный метод когда-либо называется или нет. Если переопределенный метод никогда не вызывается, то встроить метод суперкласса по-прежнему безопасно и законно! Итак, что может сделать наш умный динамический компилятор, так это встроить метод суперкласса в любом случае, но поставить небольшую проверку типов в начале, которая гарантирует, что, если объект-получатель когда-либо имеет тип подкласса, мы деоптимизируем обратно к не встроенной версии. Это называется спекулятивное встраивание, и статический компилятор AOT в корне не может этого сделать.

Полиморфное встроенное кэширование — это еще более сложная оптимизация, которую выполняют современные высокопроизводительные механизмы выполнения языков, такие как HotSpot, Rubinius или V8.

I mean, if the JVM has to compile once, run multiple time, then wouldn't this outweigh the speed-up of the optimizations done by the JVM?

Эти динамические оптимизации в корне невозможны для статического оптимизатора.

Другие вопросы по теме