Оптимизация во время выполнения статических языков: JIT для C++?

Кто-нибудь использует трюки JIT для повышения производительности во время выполнения статически скомпилированных языков, таких как C++? Похоже, что анализ горячих точек и предсказание ветвлений на основе наблюдений, выполненных во время выполнения, могут улучшить производительность любого кода, но, возможно, есть некоторая фундаментальная стратегическая причина, по которой такие наблюдения и изменения во время выполнения возможны только на виртуальных машинах. Я отчетливо помню, как подслушивающие авторы компилятора C++ бормотали: "Вы можете сделать это и для программ, написанных также на C++", слушая, как энтузиасты динамического языка говорят о сборе статистики и реорганизации кода, но мои поиски в Интернете для подтверждения этой памяти оказались безнадежными.

7 ответов

Решение

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

Вам может быть интересно искать информацию о HP Dynamo, хотя эта система ориентирована на нативный двоичный код -> нативный двоичный перевод, хотя, поскольку C++ почти исключительно компилируется в нативный код, я полагаю, это именно то, что вы ищете.

Вы также можете взглянуть на LLVM, который представляет собой среду компилятора и промежуточное представление, поддерживающее JIT-компиляцию и оптимизацию времени выполнения, хотя я не уверен, существуют ли на самом деле какие-либо среды выполнения на основе LLVM, которые могут компилировать C++ и execute + runtime оптимизировать это еще.

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

В итоге я написал свой собственный маленький Jit-компилятор для предметно-ориентированного языка (очень близко к asm, но с некоторыми структурами управления высокого уровня и локальными переменными).

Улучшение производительности, которое я получил, было между 10 и 60 факторами (в зависимости от сложности скомпилированного кода), поэтому дополнительная работа окупилась.

На ПК я бы не стал писать свой собственный jit-компилятор, а использовал бы либо LIBJIT, либо LLVM для jit-компиляции. В моем случае это было невозможно из-за того, что я работал на неосновном встроенном процессоре, который не поддерживается LIBJIT/LLVM, поэтому мне пришлось изобрести свой собственный.

Ответ более вероятен: никто не сделал больше, чем PGO для C++, потому что преимущества, вероятно, незаметны.

Позвольте мне уточнить: движки / среды выполнения JIT имеют как преимущества, так и недостатки с точки зрения их разработчика: у них больше информации во время выполнения, но гораздо меньше времени для анализа. Некоторые оптимизации действительно дороги, и вы вряд ли увидите, что без огромного влияния на время запуска вы увидите такие, как: развертывание цикла, автоматическая векторизация (которая в большинстве случаев также основана на развертывании цикла), выбор инструкций (для использования SSE4.1 для ЦП, использующий SSE4.1) в сочетании с планированием команд и переупорядочением (для использования более суперскалярных ЦП). Этот вид оптимизации прекрасно сочетается с C-подобным кодом (который доступен из C++).

Единственная полноценная архитектура компилятора для продвинутой компиляции (насколько мне известно) - это компиляция и архитектура Java Hotspot с похожими принципами с использованием многоуровневой компиляции (системы Java Azul, популярного на сегодняшний день движка JaegerMonkey JS).

Но одной из самых больших оптимизаций во время выполнения является следующее:

Полиморфное встроенное кэширование (это означает, что если вы запустите первый цикл с некоторыми типами, во второй раз код цикла будет специализированным типом, который был из предыдущего цикла, и JIT установит охрану и установит в качестве ветви по умолчанию встроенный типы и, исходя из этого, из этой специализированной формы, использующей механизм на основе SSA- формы, будет применять постоянное свертывание / распространение, вставку, оптимизацию устранения мертвого кода и в зависимости от того, насколько "продвинутым" является JIT, будет улучшаться или менее улучшенное назначение регистров ЦП.) Как вы можете заметить, JIT (горячие точки) улучшит в основном ветвящийся код, и с течением времени информация будет лучше, чем код C++, но статический компилятор, имеющий время для анализа переупорядочение инструкций для простых циклов, скорее всего, даст немного лучшую производительность. Также, как правило, код C++, области, которые должны быть быстрыми, обычно не является ООП, поэтому информация об оптимизации JIT не принесет такого удивительного улучшения.

Еще одним преимуществом JIT является то, что JIT работает на кросс-сборках, поэтому у него больше информации, если он хочет делать встраивание.

Позвольте мне уточнить: допустим, у вас есть базовый класс A, и у вас есть только одна его реализация, а именно B, в другом пакете / сборке / геме / и т.д. и загружается динамически.

JIT, поскольку он видит, что B является единственной реализацией A, он может заменять повсюду в своем внутреннем представлении вызовы A кодами B, и вызовы методов не будут выполнять диспетчеризацию (посмотрите на vtable), но будут прямыми вызовами. Эти прямые звонки также могут быть встроены. Например, у этого B есть метод: getLength() который возвращает 2, все вызовы getLength() может быть уменьшен до постоянного 2 во всем. В конце код C++ не сможет пропустить виртуальный вызов B из другой библиотеки DLL.

Некоторые реализации C++ не поддерживают оптимизацию большего количества файлов.cpp (даже сегодня в последних версиях GCC есть флаг -lto, который делает это возможным). Но если вы являетесь разработчиком C++ и беспокоитесь о скорости, вы, скорее всего, поместите все чувствительные классы в одну статическую библиотеку или даже в один и тот же файл, поэтому компилятор может легко встроить его, создавая дополнительную информацию, которую JIT имеет в своем дизайне., должен быть предоставлен самим разработчиком, поэтому без потери производительности.

Visual Studio имеет опцию для выполнения профилирования во время выполнения, которая затем может быть использована для оптимизации кода.

"Оптимизация профиля по профилю"

Microsoft Visual Studio называет это " оптимизацией по профилю"; Вы можете узнать больше об этом на MSDN. По сути, вы запускаете программу несколько раз с приложенным профилировщиком для записи его горячих точек и других характеристик производительности, а затем вы можете передать выходные данные профилировщика в компилятор для получения соответствующих оптимизаций.

Я считаю, что LLVM пытается сделать это. Он пытается оптимизировать все время жизни программы (время компиляции, время компоновки и время выполнения).

Разумный вопрос - но с сомнительной предпосылкой.

Как и в ответе Нильса, иногда "оптимизация" означает "низкоуровневая оптимизация", что само по себе является хорошим предметом.

Тем не менее, он основан на концепции "горячей точки", которая не имеет ничего общего с тем значением, которое ей обычно дают.

Определение: горячая точка - это небольшая область кода, где счетчик программы процесса тратит большой процент своего времени.

Если есть горячая точка, такая как плотный внутренний цикл, занимающий много времени, стоит попытаться оптимизировать на низком уровне, если он находится в коде, которым вы управляете (т.е. не в сторонней библиотеке).

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

Есть много распространенных способов замедлить работу программного обеспечения, одним из которых являются горячие точки. Однако, по моему опыту, это единственное, о чем знает большинство программистов, и единственное, к которому относится низкоуровневая оптимизация.

Видеть это.

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