Почему механизм выполнения LLVM быстрее, чем скомпилированный код?
У меня есть компилятор, который нацелен на LLVM, и я предоставляю два способа запуска кода:
- Запустить его автоматически. Этот режим компилирует код в LLVM и использует JIT ExecutionEngine для компиляции его в машинный код "на лету" и запускает его без создания выходного файла.
- Скомпилируйте его и запустите отдельно. В этом режиме выводится LLVM.bc файл, который я вручную оптимизирую (с помощью
opt
), скомпилируйте для собственной сборки (с llc
) компиляцию в машинный код и ссылку (с помощью gcc
) и запустите.
Я ожидал, что подход № 2 будет быстрее, чем подход №1, или, по крайней мере, с той же скоростью, но, пройдя несколько тестов скорости, я с удивлением обнаруживаю, что # 2 последовательно работает примерно в два раза медленнее. Это огромная разница в скорости.
В обоих случаях работает тот же исходный код LLVM. С подходом № 1 я еще не потрудился запускать любые пропуски оптимизации LLVM (именно поэтому я ожидал, что это будет медленнее). При подходе # 2 я запускаю opt
с -std-compile-opts
и llc
с помощью -O3
, чтобы максимизировать оптимизацию, но она не приближается к № 1. Вот пример запуска той же программы:
- # 1 без оптимизации: 11.833s
- # 2 без оптимизации: 22.262s
- # 2 с оптимизацией (
-std-compile-opts
и -O3
): 18.823s
Выполняет ли ExecutionEngine что-то особенное, о котором я не знаю? Есть ли способ оптимизировать скомпилированный код для достижения той же производительности, что и ExecutionEngine JIT?
Ответы
Ответ 1
Для виртуальной машины с JIT нормально работать с некоторыми приложениями быстрее, чем скомпилированное приложение. Это потому, что виртуальная машина с JIT похожа на симулятор, который имитирует виртуальный компьютер, а также запускает компилятор в режиме реального времени. Поскольку обе задачи встроены в виртуальную машину с JIT, машинный симулятор может передавать информацию компилятору, чтобы код можно перекомпилировать для более эффективной работы. Информация, которую он предоставляет, недоступна для статически скомпилированного кода.
Этот эффект также был отмечен с помощью виртуальных машин Java и Python PyPy VM, среди прочих.
Ответ 2
Другая проблема заключается в выравнивании кода и других оптимизаций. В настоящее время cpu настолько сложны, что трудно предсказать, какие методы приведут к более быстрому выполнению финальной бинарной версии.
Как реальный пример, позвольте рассмотреть Google Native Client - я имею в виду оригинальный подход к сборке nacl, а не LLVM (потому что, насколько мне известно, в настоящее время существует направление поддержки как "nativeclient", так и "LLVM bitcode", (modyfied)).
Как вы можете видеть на презентациях (посмотрите на youtube.com) или в papers, как этот Собственный клиент: Песочница для портативного, ненадежного родного кода x86, даже их метод выравнивания делает размер кода более крупным, в некоторых случаях такое выравнивание инструкций (например, с помощью noops) дает лучший кэш.
Выравнивание команд с помощью noop и переупорядочения команд, известных в параллельных вычислениях, и здесь оно также показывает, что это также влияет.
Надеюсь, что этот ответ дает представление о том, сколько обстоятельств может повлиять на выполнение кодовой скорости, и это множество возможных причин для разных фрагментов кода, и каждый из них нуждается в исследовании. Nevermore, это интересная тема, поэтому, если вы найдете более подробную информацию, не повторяйте свой ответ и сообщите нам в "Post-Scriptorium", что вы нашли больше:). (Может быть, ссылка на whitepaper/devblog с новыми выводами:)). Тесты всегда приветствуются - посмотрите: http://llvm.org/OpenProjects.html#benchmark.