Сентябрь 2015

ТОМ 30, НОМЕР 9

Оптимизации компилятором - Ускорение работы неуправляемого кода с помощью оптимизации на основе профиля

Хейди Брейс | Сентябрь 2015

Продукты и технологии:

Visual Studio 2013, Visual C++

В статье рассматриваются:

  • генерация файлов базы данных PGO для оптимизации на основе профиля (profile-guide optimization, PGO);
  • оснащение кода средствами мониторинга и протоколирования (instrumentation);
  • анализ кода для выявления отличий после оптимизации.

Исходный код можно скачать по ссылке

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

В этой статье обсуждается важный метод, называемый оптимизацией на основе профиля (profile-guided optimization, PGO), который может помочь блоку окончательной обработки компилятора (compiler back end) эффективнее оптимизировать код. Как показали эксперименты, выигрыш может варьироваться в пределах от 5% до 35%. Кроме того, при осторожном применении этот метод никогда не приведет к деградации производительности вашего кода.

Эта статья опирается на две предыдущие (msdn.microsoft.com/magazine/dn904673 и msdn.microsoft.com/magazine/dn973015). Если вы не знакомы с концепцией PGO, советую сначала прочитать публикацию в Visual C++ Team Blog по ссылке bit.ly/1fJn1DI.

Введение в PGO

Одна из самых важных оптимизаций, выполняемых компилятором, — замена вызовов телами функций (function inlining) или (для краткости) подстановка функций. По умолчанию компилятор Visual C++ подставляет функции до тех пор, пока размер вызывающего кода не становится слишком большим. Однако, хотя раскрываются вызовы многих функций, это полезно, только при частых их вызовах. Иначе это просто увеличивает размер кода, впустую тратит место из-за лишних команд и унифицированных кешей и раздувает рабочий набор приложения. Как компилятор узнает, часто ли осуществляется конкретный вызов? В конечном счете это зависит от аргументов, передаваемых функции.

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

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

Этот метод применим как к неуправляемому (native), так и к управляемому (managed) коду. Однако инструментарий в этих случаях различается, и поэтому здесь я буду рассматривать PGO только для неуправляемого кода, а второй вариант оставлю для другой статьи. В оставшейся части этого раздела мы обсудим, как PGO применяется к приложению.

PGO — отличный метод. Но, как и все на этом свете, он имеет свои недостатки. Он требует дополнительного времени (в зависимости от размера приложения) и усилий. К счастью, как вы увидите позже, Microsoft предоставляет инструментарий, который позволяет существенно уменьшить время на выполнение PGO для приложения. Процесс выполнения PGO состоит из трех этапов: сборки с оснащением, тренинга и сборки с PGO.

Сборка с оснащением

Есть несколько способов профилирования выполняемой программы. Компилятор Visual C++ использует статическое оснащение двоичного кода (static binary instrumentation), при котором генерируются наиболее точные профили, но времени это занимает больше. Используя оснащение, компилятор вставляет небольшое количество машинных команд в интересующие его места всех функций в вашем коде, как показано на рис. 1. Эти команды фиксируют, когда выполнялась соответствующая часть вашего кода, и включают эту информацию в генерируемый профиль.

Сборка с оснащением для приложения, оптимизируемого по методу PGO
Рис. 1. Сборка с оснащением для приложения, оптимизируемого по методу PGO

C/CPP C/CPP
c1.exe c1.exe
CIL OBJ CIL OBJ
ASM OBJ ASM OBJ
ASM/CIL LIB ASM/CIL LIB
/LTCG:PGI /PGD:app.pgd /out:app.inst.[exe|dll] /LTCG:PGI /PGD:app.pgd /out:app.inst.[exe|dll]
link.exe link.exe
All ASM OBJs & import LIBs Все ASM OBJ и LIB импорта
All CIL OBJs Все CIL OBJ
c2.exe c2.exe
Instrumented Оснащение
ASM OBJs ASM OBJs
pgort.lib & other import libraries pgort.lib и другие библиотеки импорта
dependence зависимость

Сборка оснащенной версии приложении проходит в несколько этапов. Сначала вы должны скомпилировать все файлы исходного кода с ключом /GL, который включает Whole Program Optimization (WPO). WPO необходим для оснащения программы (не с технической точки зрения, а в том смысле, что это помогает сделать генерируемый профиль гораздо более полезным). И только файлы, скомпилированные с ключом /GL, будут оснащаться.

Чтобы пройти второй этап без проблем, избегайте использования любых ключей компилятора, которые приводят к созданию дополнительного кода. Например, отключите подстановку функций (/Ob0). Кроме того, отключите проверки защиты (/GS–) и удалите проверки периода выполнения (никаких /RTC). То есть вы не должны использовать режимы Release и Debug по умолчанию в Visual Studio. В случае файлов, скомпилированных без /GL, оптимизируйте их по скорости (/O2). В случае оснащенного кода укажите хотя бы /Og.

Далее скомпонуйте сгенерированные объектные файлы и нужные статические библиотеки с ключом /LTCG:PGI. Это заставляет компоновщик (linker) выполнить три задачи. Он сообщает блоку окончательной обработки компилятора оснастить код и сгенерировать файл базы данных PGO (файл в формате PGD). Он будет использоваться на третьем этапе для сохранения всех профилей. На данный момент PGD-файл не содержит никаких профилей. В нем есть лишь информация, идентифицирующая объектные файлы, по которым при использовании PGD-файла распознается, были ли они изменены. По умолчанию имя PGD-файла соответствует имени исполняемого файла. Вы также можете указать имя PGD-файла, задав необязательный ключ /PGD компоновщика. Третья задача — связывание с библиотекой импорта pgort.lib. Выходной исполняемый файл зависит от DLL исполняющей среды PGO с именем pgortXXX.dll, где XXX — версия Visual Studio.

Конечный результат этого этапа — исполняемый файл (EXE или DLL), заполненный кодом оснащения, и пустой PGD-файл, который заполняется и используется на третьем этапе. Статическую библиотеку можно оснастить, только если она компонуется (связывается) с оснащаемым проектом. Кроме того, все файлы CIL OBJ должны генерироваться той же версией компилятора, а иначе компоновщик выдаст ошибку.

Пробники для профилирования

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

Для записи профиля компилятор вставляет пробники (probes) в каждую функцию, скомпилированную с ключом /GL. Пробник — это небольшая последовательность команд (от двух до четырех инструкций), состоящая из нескольких команд заталкивания (push) и одной команды вызова (call) обработчика пробника в конце. При необходимости пробник обертывается вызовами двух функций для сохранения и восстановления всех регистров XMM. Существует три типа пробников.

  • Пробник-счетчик (count probes): Это самый распространенный тип пробника. Он подсчитывает, сколько раз выполнялся блок кода, увеличивая счетчик на 1 при каждом выполнении. Он также дает минимальные издержки в плане размера и скорости. Каждый счетчик имеет размер в восемь байтов на процессорах x64 и четыре байта на процессорах x86.
  • Пробники входа (entry probes): Компилятор вставляет пробник входа в начало каждой функции. Цель этого пробника — сообщать другим пробникам в той же функции использовать счетчики, сопоставленные с этой функцией. Это необходимо потому, что обработчики пробников являются общими для пробников во всех функциях. Пробник входа функции main инициализирует исполняющую среду PGO. Этот пробник также является счетчиком. И он самый медленный.
  • Пробники значений (value probes): Эти пробники вставляются перед каждым вызовом виртуальной функции и каждым выражением switch и используются для записи гистограммы значений. Пробник значений также является счетчиком, поскольку он подсчитывает, сколько раз появляется каждое значение. Он имеет самый большой размер.

Функция не будет оснащаться никакими пробниками, если в ней только один базовый блок (последовательность команд с одним входом и одним выходом). По сути, она могла бы быть подставлена несмотря на ключ /Ob0. Помимо пробника значений, каждое выражение switch заставляет компилятор создать константный раздел COMDAT, описывающий это выражение. Размер этого раздела примерно равен количеству блоков case, помноженному на размер переменной, контролирующей это выражение switch.

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

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

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

Все обработчики пробников в какой-то момент увеличивают счетчик на единицу для фиксации выполнения соответствующего блока кода. Компилятор использует инструкцию ADD для приращения четырехбайтового счетчика, а на платформе x64 инструкция ADC дополнительно добавляет разряд переноса (carry) в старший четвертый байт счетчика. Эти инструкции небезопасны в многопоточной среде. А значит, все пробники по умолчанию тоже небезопасны в такой среде. Если хотя бы одна функция может быть выполнена более чем одним потоком одновременно, результаты окажутся ненадежными. В таком случае можно использовать ключ /pogosafemode компоновщика. Это заставляет компилятор ставить префикс LOCK перед этими инструкциями, что делает все пробники безопасными в многопоточной среде. Конечно, это замедляет их выполнение. К сожалению, избирательное применение этого функционала невозможно.

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

Этап тренинга

По окончании первого этапа вы получаете оснащенную версию исполняемого файла и PGD-файл. Второй этап — это тренинг, в ходе которого исполняемый файл будет генерировать один или более профилей, которые сохраняются в отдельном файле PGO Count (PGC). Эти файлы будут задействованы на третьем этапе для оптимизации кода.

Это самый важный этап, так как точность профиля критически важна для успеха всего процесса. Чтобы профиль был полезен, он должен отражать наиболее распространенный сценарий использования программы. Компилятор будет оптимизировать программу, предполагая, что отработанные сценарии являются распространенными. Если это не так, ваша программа может даже замедлиться. Профиль, сгенерированный по распространенному сценарию использования, помогает компилятору выявить «горячие пути», которые следует оптимизировать по скорости, и «холодные пути», которые должны быть оптимизированы по размеру, как показано на рис. 2.

Этап тренинга для создания PGO-приложения
Рис. 2. Этап тренинга для создания PGO-приложения

Time Время
Usage scenario 1 starts Запускается сценарий использования 1
Usage scenario 2 starts Запускается сценарий использования 2
Usage scenario 1 ends Завершается сценарий использования 1
Usage scenario 2 ends Завершается сценарий использования 2

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

Компилятор будет оптимизировать программу, предполагая, что отработанные сценарии являются распространенными.

В комплексном сценарии использования на рис. 2 pgosweep.exe — это утилита командной строки, которая позволяет контролировать содержимое профиля, поддерживаемое исполняющей средой PGO при выполнении. Вы можете создать более одного экземпляра программы и параллельно применять сценарии использования.

Вообразите, что у вас выполняются два экземпляра в процессах X и Y. Когда по первому сценарию предполагается запуск процесса X, вызовите pgosweep и передайте ей идентификатор этого процесса и ключ /onlyzero. Это заставит исполняющую среду PGO очистить часть профиля в памяти только для этого процесса. Без указания идентификатора процесса будет очищен весь PGC-профиль. После этого можно запускать данный сценарий. Аналогичным образом можно инициировать второй сценарий в процессе Y.

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

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

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

Сборка с PGO

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

Первый шаг в сборке PGO-версии — объединение всех PGC-файлов с помощью утилиты командной строки pgomgr.exe. Она также позволяет редактировать PGD-файл. Чтобы объединить два PGC-файла в PGD-файл, сгенерированный на первом этапе, запустите pgomgr и передайте ключ /merge и имя PGD-файла. Это приведет к объединению всех PGC-файлов в текущем каталоге, имена которых совпадают с именем указанного PGD-файла и отличаются только суффиксом !# и числом. Компилятор и компоновщик могут использовать конечный PGD-файл для оптимизации кода.

С помощью утилиты pgomgr можно захватить более распространенный или важный сценарий использования. Для этого передайте имя соответствующего PGC-файла и ключ /merge:n, где n — положительное целое число, указывающее количество копий PGC-файла для включения в PGD-файл. По умолчанию n равно единице. Увеличение этого значения заставляет процесс оптимизации смещаться в пользу этого специфического профиля.

Второй шаг — запуск компоновщика с передачей того же набора объектных файлов, что и на первом этапе. На этот раз используйте ключ /LTCG:PGO. Компоновщик будет искать PGD-файл с тем же именем, что и у исполняемого файла, в текущем каталоге. Это гарантирует, что CIL OBJ-файлы не изменятся с момента генерации PGD-файла на первом этапе, а затем будут переданы компилятору для использования и оптимизации кода. Этот процесс показан на рис. 3. Вы можете использовать ключ /PGD компоновщика, чтобы явным образом указать PGD-файл. Не забудьте разрешить подстановку функций на этом этапе.

Сборка с PGO на третьем этапе
Рис. 3. Сборка с PGO на третьем этапе

appname|#1.pgc appname|#1.pgc
appname|#2.pgc appname|#2.pgc
/merge /merge
pgomgr.exe pgomgr.exe
appname.pgd appname.pgd
pgodbXXX.dll pgodbXXX.dll
/LTCG:PGO /PGD:app.pgd /out:app.[exe|dll] /LTCG:PGO /PGD:app.pgd /out:app.[exe|dll]
link.exe link.exe
All CIL OBJs Все CIL OBJ-файлы
c2.exe c2.exe
Optimized Оптимизированные
ASM OBJs ASM OBJs
appname.[exe|dll] appname.[exe|dll]

Большинство оптимизация компилятором и компоновщиком станут управляемыми профилем. Конечный результат этого этапа — в высшей степени оптимизированный исполняемый файл по размеру и скорости работы. И теперь будет неплохо замерить выигрыш в производительности.

Поддержание кодовой базы

Если вы вносите любые изменения в любые входные файлы, передаваемые компоновщику с ключом /LTCG:PGI, то компоновщик откажется использовать PGD-файл, если указан ключ /LTCG:PGO. Дело в том, что такие изменения могут существенно повлиять на пригодность PGD-файла.

Один из вариантов — повторять три ранее описанных этапа для генерации другого совместимого PGD. Однако, если изменения были минимальными (например, добавлено малое количество функций, какая-то функция стала вызываться чуть реже или чуть чаще, либо добавлена функциональность, используемая не часто), тогда повторять весь процесс непрактично. В этом случае можно использовать ключ /LTCG:PGU вместо ключа /LTCG:PGO. Это сообщает компоновщику пропускать проверки совместимости в PGD-файле.

Со временем такие мелкие изменения накапливаются. И в конечном счете вы достигнете такой точки, в которой будет иметь смысл заново оснастить приложение. Определить достижение этой точки можно наблюдением за выводом компилятора при сборке кода с PGO. Это покажет вам, какую часть кодовой базы охватывает PGD. Если охват профилем падает ниже 80% (рис. 4), следует заново оснастить код. Но этот процент сильно зависит от природы приложения.

Рис. 4. Цикл сопровождения кодовой базы с PGO

Instrumentation Build Сборка с оснащением
Training Тренинг
PGO Build (/LTCG:PGO Сборка с PGO (/LTCG:PGO)
Code Base Maintenance Сопровождение кодовой базы
False False
Coverage >= 80% Охват >= 80%
True True
PGO Build (/LTCG:PGU) Сборка с PGO (/LTCG:PGU)

PGO в действии

Оптимизации на основе PGO применялись компилятором и компоновщиком. Я задействую симулятор NBody, чтобы продемонстрировать некоторые его преимущества. Вы можете скачать это приложение с bit.ly/1gpEaCY. Кроме того, вы должны скачать и установить DirectX SDK (bit.ly/1LQnKge), чтобы компилировать это приложение.

Сначала я скомпилирую приложение в режиме Release, чтобы сравнить его с PGO-версией. Чтобы собрать PGO-версию приложения, можно использовать команду Profile-Guided Optimization меню Build в Visual Studio.

Вы также должны включить ассемблерный вывод, используя ключ /FA[c] компилятора (не используйте /FA[c]s в этой демонстрации). Для этого простого приложения достаточно выполнить тренинг оснащенного приложения всего один раз, чтобы сгенерировать один PGC-файл и использовать его для оптимизации приложения. Тем самым вы получите два исполняемых файла: один оптимизирован вслепую, а другой — на основе PGO. Убедитесь, что вам доступен конечный PGD-файл, так как он понадобится вам позднее.

Теперь, если вы последовательно запустите оба исполняемых файла и сравните полученные счетчики GFLOP, то заметите, что производительность похожа. На первый взгляд кажется, что применение PGO к приложению было пустой тратой времени. Но при более глубоком анализе оказывается, что размер приложения сократился с 531 Кб (для версии, оптимизированной вслепую) до 472 Кб (для PGO-версии), или на 11%. Таким образом, когда вы применили PGO к этому приложению, она сократилось по размеру, сохранив практически ту же производительность. Как это случилось?

Возьмем 200-строчную функцию DXUTParseCommandLine из файла DXUT/Core/DXUT.CPP. Глядя на сгенерированный ассемблерный код при компиляции в режиме Release, можно заметить, что размер двоичного кода составляет примерно 2700 байтов. С другой стороны, размер двоичного кода в PGO-версии не превышает 1650 байтов. Причина этой разница понятна по ассемблерной инструкции, которая проверяет условие следующего цикла:

for( int iArg = iArgStart; iArg < nNumArgs; iArg++ ) { ... }

Оптимизация вслепую дает такой код:

0x044 jge            block1
; Пропускаемый код выполняется, когда iArg < nNumArgs
; В промежутке находится масса другого кода
0x362 block1:
; iArg >= nNumArgs
; Масса другого кода

С другой стороны, сборка с PGO генерирует следующий код:

0x043 jl  block1
; Взят - 0(0%), не взят - 1(100%)
block2:
; Пропускаемый код выполняется, когда iArg >= nNumArgs
0x05f ret 0
; Ниже находится "мертвый код"
0x000 block1:
; Масса другого кода выполняется, когда iArg < nNumArgs

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

Вы можете заметить другое интересное отличие из фрагментов ассемблерного кода. В исполняемом файле, оптимизированном вслепую, ветвь, которая редко выполняется, находится в пропускаемом пути (fall-through path) условной инструкции. Ветвь, которая выполняется почти всегда, расположена в 800 байтах от условной инструкции. Это не только приводит к провалу блока предсказания ветвления в процессоре (processor branch predictor), но и гарантирует инструкции промах кеша (cache miss).

Сборка с PGO предотвращает обе эти проблемы, меняя местами эти ветвления. Фактически PGO перемещает редко выполняемую ветвь в отдельный раздел исполняемого файла, тем самым улучшая локальность рабочего набора. Эта оптимизация называется отделением мертвого (не выполняемого) кода (dead code separation). Добиться этого без профиля было бы невозможно.

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

Рассмотрим чуть более интересную функцию, DXUTStaticWndProc, которая определена в том же файле. Структура управления функций выглядит так:

if (condition1) { /* много кода */ }
if (condition2) { /* много кода */ }
if (condition3) { /* много кода */ }
switch (variable) { /* много блоков case
                       со множеством кода в каждом */ }
if-else statement
return

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

if (condition1) { goto dead-code-section }
if (condition2) { goto dead-code-section }
if (condition3) { /* много кода */ }
{/* Часто выполняемые блоки case вынесены
    за пределы выражения switch */}
if-else statement
return
switch(variable) { /* остальные блоки case */ }

Дополнительные ресурсы

Подробнее о базах данных оптимизации на основе профилей см. статью Хейди Брейса в блоке по ссылке bit.ly/1KBcffQ.

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

Базы данных PGO

Преимущества PGD-профиля значительно выходят за рамки простого управления оптимизаций компилятором. Хотя вы можете использовать pgomgr.exe для слияния нескольких PGC-файлов, она также служит другой цели. Эта утилита предлагает три ключа, которые позволяют просматривать содержимое PGD-файла для получения полного представления о поведении вашего кода с учетом используемых сценариев. Ключ /summary сообщает утилите сгенерировать текстовую сводку содержимого PGD-файла. Ключ /detail, применяемый в сочетании с первым, указывает утилите создать подробное текстовое описание профиля. Наконец, ключ /unique сообщает утилите убрать дополнения с имен функций (undecorated) (особенно полезно для кодовых баз на C++).

Программное управление

Осталось упомянуть о последней функциональности. Заголовочный файл pgobootrun.h объявляет одну функцию — PgoAutoSweep. Вы можете вызывать эту функцию для программной генерации PGC-файла и очистки профиля в памяти для подготовки к следующему PGC-файлу. Эта функция принимает один аргумент типа char*, ссылающийся на имя PGC-файла. Чтобы использовать эту функцию, вы должны выполнить связывание со статической библиотекой pgobootrun.lib. В настоящее время это единственная программная поддержка, относящаяся к PGO.

Заключение

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

Однако вы можете получить более богатый набор средств, используя плагин PGO, который можно скачать по ссылке bit.ly/1Ntg4Be. Он хорошо документирован на bit.ly/1RLjPDi. Не забывайте о пороговом значении охвата с рис. 4, и самый простой способ получить его — использовать плагин, как описано в документации. Но, если вы предпочитаете утилиты командной строки, то можете прочитать статью на bit.ly/1QYT5nO — в ней есть множество примеров. Если вы располагаете неуправляемой кодовой базой, сейчас самое время опробовать этот метод. Сделав это, не колеблясь дайте мне знать, как он повлиял на размер и скорость вашего приложения.

PGO Code Base Maintenance Cycle
Figure 4 PGO Code Base Maintenance Cycle

Additional Resources

For more information on profile-guided optimization databases, check out the blog post from Hadi Brais.


Хейди Брейс (Hadi Brais) — аспирант в Индийском технологическом институте Дели (Indian Institute of Technology Delhi, IITD), исследует оптимизации компилятора для технологий памяти следующего поколения. Большую часть времени проводит в написании кода на C/C++/C# и глубоко копает в CLR и CRT. Ведет блог hadibrais.wordpress.com. С ним можно связаться по адресу hadi.b@live.com.

Выражаю благодарность за рецензирование статьи эксперту Microsoft Анкиту Астану (Ankit Asthana).