Поделиться через


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

В данном документе описаны рекомендации по оптимизации в Visual C++. Рассматриваются следующие вопросы:

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

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

    • Выбор используемого уровня оптимизации

    • Переключатели для значений с плавающей запятой

  • Оптимизация спецификаций объявлений

  • Прагмы оптимизации

  • Ключевые слова __restrict и __assume

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

  • Исключения

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

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

Visual C++ поддерживает профильную оптимизацию. Эта оптимизация использует данные профиля, собранные во время прошлых запусков инструментированной версии, чтобы впоследствии их использовать для оптимизации приложения. Использование вероятностного оптимизатора (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 (Определение поведения с плавающей запятой).

Оптимизация спецификаций объявлений

В данном разделе рассматриваются две спецификации объявлений, которые могут использоваться в программах для повышения производительности: __declspec(restrict) и __declspec(noalias).

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

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

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

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

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

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

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

Прагмы оптимизации

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

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

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

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

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

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

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

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

Ключевые слова __restrict и __assume

В Visual C++ существует два ключевых слова, которые используются для повышения производительности: __restrict и __assume.

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

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

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

Ключевое слово __restrict заменяет параметр /Oa, который использовался в предыдущих версиях.

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

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

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

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

Встроенные функции — это вызовы функций, при которых компилятор имеет внутренние сведения о вызове и вместо вызова функции из библиотеки создает код для этой функции. Файл заголовка Intrin.h, расположенный в <Installation_Directory>\VC\include\intrin.h, содержит доступные встроенные функции для каждой из трех поддерживаемых платформ (x86, x64 и Itanium).

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

  1. Код является легче переносимым. Некоторые встроенные функции доступны для архитектуры с несколькими процессорами.

  2. Код является удобочитаемым, поскольку он написан на C/C++.

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

Дополнительные сведения см. в разделах Compiler Intrinsics и Benefits of Using Intrinsics.

Исключения

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

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

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

См. также

Ссылки

Оптимизация кода