Имейте в виду, что ниже приведено только сравнение различий между нативной и JIT-компиляцией, и оно не охватывает специфику какого-либо конкретного языка или фреймворков. Могут быть законные причины для выбора конкретной платформы за пределами этого.
Когда мы утверждаем, что нативный код работает быстрее, мы говорим о типичном случае использования нативно скомпилированного кода в сравнении с JIT-скомпилированным кодом, когда типичное использование JIT-скомпилированного приложения должно выполняться пользователем с немедленными результатами (например, нет сначала жду компилятора). В этом случае, я не думаю, что кто-то может с уверенностью заявить, что JIT-скомпилированный код может соответствовать или превосходить нативный код.
Давайте предположим, что у нас есть программа, написанная на каком-то языке X, и мы можем скомпилировать ее с собственным компилятором и снова с JIT-компилятором. Каждый рабочий процесс включает в себя одни и те же этапы, которые можно обобщить как (Код -> Промежуточное представление -> Машинный код -> Выполнение). Большая разница между двумя заключается в том, какие этапы видит пользователь, а какие - программист. При нативной компиляции программист видит все, кроме стадии выполнения, но с JIT-решением, компиляция в машинный код просматривается пользователем в дополнение к выполнению.
Заявление о том, что A быстрее, чем B , относится к времени, затраченному на выполнение программы, как это видит пользователь . Если мы предположим, что обе части кода работают одинаково на этапе выполнения, мы должны предположить, что рабочий процесс JIT медленнее для пользователя, поскольку он также должен видеть время T компиляции в машинный код, где T> 0. Так что чтобы любая возможность рабочего процесса JIT выполнять то же, что и собственный рабочий процесс, для пользователя, мы должны уменьшить время выполнения кода, чтобы выполнение + компиляция в машинный код было меньше, чем только этап выполнения родного рабочего потока. Это означает, что мы должны оптимизировать код лучше в JIT-компиляции, чем в нативной компиляции.
Это, однако, довольно невыполнимо, поскольку для выполнения необходимых оптимизаций для ускорения выполнения мы должны тратить больше времени на этап компиляции для машинного кода и, следовательно, любое время, которое мы экономим в результате оптимизированного кода, фактически теряется, так как мы добавляем его в сборник. Другими словами, «медлительность» решения на основе JIT не только из-за дополнительного времени для JIT-компиляции, но и кода, создаваемого этой компиляцией, работает медленнее, чем собственное решение.
Я буду использовать пример: распределение регистра. Поскольку доступ к памяти в несколько тысяч раз медленнее, чем доступ к регистрам, в идеале мы хотим использовать регистры везде, где это возможно, и иметь как можно меньше обращений к памяти, но у нас ограниченное количество регистров, и мы должны выливать состояние в память, когда нам нужно регистр. Если мы используем алгоритм распределения регистров, который требует 200 мс для вычисления, и в результате мы экономим 2 мс времени выполнения - мы не будем оптимально использовать время для JIT-компилятора. Такие решения, как алгоритм Чейтина, который может генерировать высоко оптимизированный код, не подходят.
Роль JIT-компилятора состоит в том, чтобы найти лучший баланс между временем компиляции и качеством создаваемого кода, однако с большим смещением на быстрое время компиляции, так как вы не хотите оставлять пользователя в ожидании. Производительность исполняемого кода ниже в случае JIT, поскольку собственный компилятор не ограничен (в значительной степени) временем при оптимизации кода, поэтому может свободно использовать лучшие алгоритмы. Вероятность того, что общая компиляция + выполнение для JIT-компилятора может превзойти только время выполнения для скомпилированного кода, фактически равна 0.
Но наши виртуальные машины не ограничиваются компиляцией JIT. Они используют опережающие методы компиляции, кэширование, горячую замену и адаптивную оптимизацию. Итак, давайте изменим наше утверждение, что производительность - это то, что видит пользователь, и ограничим его временем, затрачиваемым на выполнение программы (предположим, что мы скомпилировали AOT). Мы можем эффективно сделать исполняемый код эквивалентным нативному компилятору (или, может быть, лучше?). Большим преимуществом для виртуальных машин является то, что они могут создавать код более высокого качества, чем собственный компилятор, поскольку он имеет доступ к большему количеству информации - информации о выполняющемся процессе, например о том, как часто может выполняться определенная функция. Затем виртуальная машина может применить адаптивную оптимизацию к наиболее важному коду с помощью горячей замены.
Однако есть проблема с этим аргументом - он предполагает, что оптимизация на основе профилей и тому подобное является чем-то уникальным для виртуальных машин, что не соответствует действительности. Мы также можем применить его к собственной компиляции - скомпилировав наше приложение с включенным профилированием, записав информацию, а затем перекомпилировав приложение с этим профилем. Вероятно, также стоит отметить, что горячая замена кода - это не то, что может сделать только JIT-компилятор, мы можем сделать это для собственного кода - хотя решения на основе JIT для этого более доступны и намного проще для разработчика. Итак, главный вопрос: может ли виртуальная машина предложить нам некоторую информацию, которую не может дать нативная компиляция, которая может повысить производительность нашего кода?
Я не могу видеть это сам. Мы можем применить большинство методов типичной виртуальной машины и к собственному коду, хотя этот процесс более сложный. Точно так же мы можем применить любые оптимизации собственного компилятора обратно к виртуальной машине, которая использует компиляцию AOT или адаптивные оптимизации. Реальность такова, что разница между нативным кодом и виртуальной машиной не так велика, как мы привыкли верить. В конечном итоге они приводят к одному и тому же результату, но используют другой подход, чтобы достичь этого. ВМ использует итеративный подход для создания оптимизированного кода, когда собственный компилятор ожидает его с самого начала (и может быть улучшен с помощью итеративного подхода).
Программист C ++ может утверждать, что ему нужны оптимизации с самого начала, и он не должен ждать, пока виртуальная машина решит, как это сделать, если вообще будет. Это, вероятно, справедливо для нашей современной технологии, поскольку текущий уровень оптимизации в наших виртуальных машинах уступает тому, что могут предложить нативные компиляторы, но это может не всегда иметь место в случае улучшения решений AOT в наших виртуальных машинах и т. Д.