Влияет ли оптимизация на основе профилей, выполняемая компилятором, на случаи, не охваченные набором данных профилирования? - PullRequest
5 голосов
/ 20 октября 2011

Этот вопрос не является специфическим для C ++, AFAIK определенные среды выполнения, такие как Java RE, могут выполнять профилированную оптимизацию на лету, мне это тоже интересно.

MSDN описывает PGO например:

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

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

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

А что, если этот замедленный код часто выполняется на другом наборе данных, который программа увидит в реальном мире?Будет ли снижена производительность программы по сравнению с программой, скомпилированной без PGO, и насколько вероятно будет ухудшение?Другими словами, действительно ли PGO улучшает мою производительность кода для набора данных профилирования и, возможно, ухудшает его для других наборов данных?Есть ли реальные примеры с реальными данными?

Ответы [ 2 ]

9 голосов
/ 06 декабря 2011

Отказ от ответственности: Я не сделал больше с PGO, чем прочитал его и попробовал один раз с примером проекта для забавы.Многое из следующего основано на моем опыте с оптимизациями, не связанными с PGO, и образованными догадками.TL; DR ниже.

На этой странице перечислены оптимизации, выполненные PGO.Давайте посмотрим на них один за другим (сгруппированные по результативности):

Inlining - Например, если существует функция A, которая часто вызывает функцию B,и функция B относительно мала, тогда оптимизация на основе профиля встроит функцию B в функцию A.

Распределение регистров - Оптимизация с данными профиля приводит к лучшему распределению регистров.

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

Это, очевидно, улучшает прогнозирование окупаемости некоторых оптимизаций.Нет прямого компромисса для непрофилированных путей кода.


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

Расположение функций - На основе графика вызовов и профилированного поведения вызывающего абонента / вызываемого абонента функции, которые имеют тенденциювдоль одного и того же пути выполнения размещаются в одном и том же разделе.

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

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

Все это может уменьшить локальность непрофилированных путей кода.По моему опыту, влияние было бы заметным или серьезным, если бы этот путь кода имел тесную петлю, которая превышала бы кэш кода L1 (и, возможно, даже превышал уровень L2).Это похоже на путь, который должен был быть включен в профиль PGO:)

Разделение мертвого кода может оказать огромное влияние - в обоих направлениях - потому что оно может уменьшить доступ к диску.

Если вы полагаетесь на быстрые исключения, вы делаете это неправильно.


Оптимизация размера / скорости - Функции, в которых программа тратит много времени, могут быть оптимизированы по скорости.

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


условная оптимизация ветвления - С помощью тестов значений оптимизация на основе профилей может определить, используется ли заданное значение в операторе switch чаще, чем другие значения. Затем это значение можно извлечь иззаявление ведьмы.То же самое можно сделать с инструкциями if / else, где оптимизатор может упорядочить if / else так, чтобы блок if или else размещался первым в зависимости от того, какой блок чаще всего равен true.

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

Типичным случаем, когда это может заплатить много, является проверка параметров / диапазона времени выполнения и аналогичные пути, которые никогда не должны использоваться при нормальном выполнении.

Случай взлома будет:

if (x > 0) DoThis() else DoThat();

в соответствующем узком цикле и профилируя только случай x> 0.


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

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

Пример: - это все «обоснованное предположение», но я думаю, что это довольно показательно для всей темы.

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

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

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

Однако штраф сравнительно невелик: в общем случае без PGO вы либо всегда вызываете full memmove, либо nline полной реализации memmove. Оптимизация добавляет несколько инструкций (включая условный переход) к чему-то довольно сложному, я бы предположил, что не более 10% накладных расходов. В большинстве случаев эти 10% будут ниже уровня шума из-за доступа к кешу.

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

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


Вывод:

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

В моей интуиции я бы сказал, что

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

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

1 голос
/ 27 ноября 2011

PGO, безусловно, может влиять на время выполнения кода, который запускается реже.В конце концов, вы изменяете месторасположение некоторых функций / блоков, и это сделает блоки, которые теперь вместе, более дружественными к кешу.

Что я видел, так это то, что команды определяют свои высокоприоритетные сценарии.Затем они запускают их, чтобы обучить профилировщик оптимизации и измерить улучшение.Вы не хотите запускать все сценарии в PGO, потому что если вы это сделаете, вы также можете не запускать их.

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

...