Рекомендации по оптимизации

В этом документе описываются некоторые рекомендации по оптимизации программ C++ в Visual Studio.

Параметры компилятора и компоновщика

Профильная оптимизация

В Visual Studio поддерживается профильная оптимизация (PGO). Она использует данные профиля из обучающих выполнений инструментированной версии приложения для последующей оптимизации приложения. Использование профильной оптимизации может занимать много времени, поэтому к ней может обращаться не каждый разработчик, однако рекомендуется прибегать к такой оптимизации для окончательной сборки выпуска продукта. Дополнительные сведения см. в статье Профильные оптимизации.

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

Дополнительные сведения см. в статьях /GL (оптимизация всей программы) и /O1, /O2 (минимизация размера, максимизация скорости).

Какой уровень оптимизации следует использовать

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

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

Для связывания в 64-разрядных средах рекомендуется использовать параметр компоновщика /OPT:REF,ICF, а в 32-разрядных средах рекомендуется использовать /OPT:REF. Дополнительные сведения см. в статье Параметр /OPT (оптимизация).

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

Параметры с плавающей запятой

Параметр компилятора /Op был удален. Добавлены следующие четыре параметра компилятора для оптимизаций с плавающей запятой:

Параметр Описание
/fp:precise Это рекомендация по умолчанию, которую следует использовать в большинстве случаев.
/fp:fast Рекомендуется, если производительность имеет первостепенное значение, например в играх. При использовании этого параметра достигается максимальная производительность.
/fp:strict Рекомендуется, если требуются точные исключения для плавающей запятой и требуется поведение IEEE. При использовании этого параметра достигается минимальная производительность.
/fp:except[-] Его можно использовать вместе с /fp:strict или /fp:precise, но не с /fp:fast.

Дополнительные сведения см. в статье /fp (определение поведения с плавающей запятой).

declspecs оптимизации

В этом разделе мы рассмотрим два declspec, которые можно использовать в программах для повышения производительности: __declspec(restrict) и __declspec(noalias).

declspec restrict можно применять только к объявлениям функций, которые возвращают указатель, например __declspec(restrict) void *malloc(size_t size);.

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

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

Стоит отметить, что это гарантированно предоставляемая информация, а не данные, которые компилятору нужно проверять. Если программа использует declspec restrict ненадлежащим образом, она может работать некорректно.

Дополнительные сведения см. в разделе restrict.

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

Дополнительные сведения см. в разделе noalias.

Директивы pragma оптимизации

Существует несколько полезных директив pragma, помогающих оптимизировать код. Сначала мы рассмотрим #pragma optimize.

#pragma optimize("{opt-list}", on | off)

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

#pragma optimize("", off)
int myFunc() {...}
#pragma optimize("", on)

Дополнительные сведения см. в разделе optimize.

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

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

Еще одна полезная директива pragma для ограничения глубины встраивания — #pragma inline_depth. Ее удобно использовать в ситуациях, когда нужно ограничить размер программы или функции. Дополнительные сведения см. в разделе inline_depth.

__restrict и __assume

В Visual Studio существует несколько ключевых слов, которые могут помочь повысить производительность: __restrict и __assume.

Во-первых, следует отметить, что __restrict и __declspec(restrict) — это две разные вещи. Несмотря на некоторую связь, эти ключевые слова имеют разную семантику. __restrict является квалификатором типа, как, например, const или volatile, но исключительно для типов указателей.

Указатель, который изменяется с помощью __restrict, называется указателем __restrict. Указатель __restrict — это указатель, к которому можно обращаться только через указатель __restrict. Другими словами, другой указатель нельзя использовать для доступа к данным, на которые указывает указатель __restrict.

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

С помощью ключевого слова __assume разработчик может сообщить компилятору о необходимости допущений относительно значения определенной переменной.

Например, __assume(a < 5); указывает оптимизатору, что в этой строке кода переменная a меньше 5. Опять же, это точные данные для компилятора. Если на самом деле переменная a в этой строке в программе равна 6, программа после оптимизации, выполненной компилятором, может работать неправильно. Ключевое слово __assume максимально эффективно, если используется перед операторами switch и (или) условными выражениями.

Однако для __assume действуют некоторые ограничения. Во-первых, поскольку __restrict является всего лишь предложением, компилятор может игнорировать его. Кроме того, ключевое слово __assume сейчас работает только с переменными неравенствами для констант. Оно не распространяет символическое неравенство, например, предположим(a < b).

Поддержка встроенных функций

Встроенные функции — это вызовы функций, когда компилятор обладает внутренними знаниями о вызове, и вместо вызова функции в библиотеке он выдает код для этой функции. Файл <заголовка intrin.h> содержит все доступные встроенные компоненты для каждой поддерживаемой аппаратной платформы.

Встроенные функции позволяют программисту более подробно изучать код без использования сборки. Использование встроенных функций имеет несколько преимуществ:

  • Код является более переносимым. Ряд встроенных функций доступен в нескольких архитектурах ЦП.

  • Код более удобен для чтения, так как он по-прежнему создается на C или C++.

  • Код получает преимущество оптимизаций компилятора. По мере повышения эффективности компилятора совершенствуется процесс создания кода для встроенных функций.

Дополнительные сведения см. в статье Встроенные функции компилятора.

Исключения

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

Поэтому рекомендуется избегать добавления блоков try/catch в код, в котором они на самом деле не нужны. Если необходимо использовать исключения, по возможности используйте синхронные исключения. Дополнительные сведения см. в разделе Structured Exception Handling (C/C++).

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

См. также