Почему Java быстрее с использованием JIT, чем при компиляции в машинный код?
У меня возник вопрос о скорости выполнения Java-программ. Я слышал, что для достижения высокой производительности Java необходимо использовать JIT-компиляцию. Это вполне логично, когда мы сравниваем с интерпретацией, но почему никто не может разработать компилятор, который бы заранее компилировал Java-код в быстрый машинный код? Я знаю о gcj
, но, как я понимаю, его выходной код обычно не быстрее, чем Hotspot.
Существуют ли особенности языка, которые усложняют эту задачу? Я считаю, что дело, в первую очередь, касается следующих моментов:
- Рефлексия
- Загрузка классов
Что я упустил? Если я избегу использования этих функций, возможно ли компилировать Java-код один раз в нативный машинный код и использовать его без повторной компиляции?
5 ответ(ов)
JIT-компилятор может работать быстрее, потому что машинный код генерируется непосредственно на той машине, на которой он будет исполняться. Это означает, что JIT располагает самой точной информацией о целевой среде, что позволяет ему производить оптимизированный код.
Если же вы предварительно компилируете байт-код в машинный код, компилятор не сможет оптимизировать под конкретные целевые машины, а будет ориентироваться только на машину, на которой он был собран.
Настоящая проблема для любого компилятора AOT (Ahead-of-Time) заключается в следующем:
Class.forName(...)
Это означает, что вы не сможете создать компилятор AOT, который охватит ВСЕ программы на Java, так как существует информация, доступная только во время выполнения, касающаяся характеристик программы. Тем не менее, вы можете реализовать это для подмножества Java, что, как я считаю, и делает gcj.
Еще один типичный пример - это способность JIT (Just-In-Time) компилятора встраивать методы, такие как getX(), непосредственно в вызывающие методы, если он определяет, что это безопасно, и в случае необходимости отменять это, даже если программист явно не указал, что метод является final. JIT может видеть, что в запущенной программе данный метод не переопределяется и, следовательно, в этом случае его можно рассматривать как final. В следующем вызове все может измениться.
Правка 2019: Oracle представила GraalVM, который позволяет выполнять компиляцию AOT для подмножества Java (довольно крупного, но все еще подмножества), с основным требованием, что весь код должен быть доступен на этапе компиляции. Это позволяет достигать времени старта в миллисекунды для веб-контейнеров.
Java JIT компилятор также является ленивым и адаптивным.
Ленивый
Как ленивый компилятор, он компилирует методы только тогда, когда к ним действительно обращаются, вместо того чтобы компилировать всю программу целиком. Это особенно полезно, если какая-то часть программы не используется. Загрузка классов помогает ускорить работу JIT, позволяя ему игнорировать классы, с которыми он еще не сталкивался.
Адаптивный
Будучи адаптивным, он сначала создает быструю и "грязную" версию машинного кода, а затем, если метод используется часто, возвращается и делает более тщательную оптимизацию.
В итоге все сводится к тому, что наличие большей информации позволяет проводить более эффективные оптимизации. В данном случае JIT-компилятор располагает более подробной информацией о реальной машине, на которой выполняется код (как отметил Эндрю), а также имеет много информации о выполнении в реальном времени, которая недоступна на этапе компиляции.
Java имеет возможность выполнять инлайнинг через границы виртуальных методов и осуществлять эффективный диспетчеризацию интерфейсов, что требует анализа в процессе выполнения перед компиляцией — другими словами, для этого необходим JIT (Just-In-Time) компилятор. Поскольку все методы являются виртуальными, а интерфейсы используются повсеместно, это оказывает значительное влияние на производительность.
Почему 2 * (i * i) быстрее, чем 2 * i * i в Java?
"В чем разница между JIT-компилятором и интерпретатором?"
Инициализация ArrayList в одну строчку
Почему нет ConcurrentHashSet, если есть ConcurrentHashMap?
Создание репозитория Spring без сущности