Що кожен програміст повинен знати про оптимізації компілятора

Високорівневі мови програмування містять у собі багато абстрактних програмістських конструкцій, таких як функції, умовні оператори та цикли — вони роблять нас напрочуд продуктивними. Однак, одним з недоліків написання коду на высокоуровневом мовою є потенційне значне зниження швидкості роботи програми. Тому компілятори намагаються автоматично оптимізувати код і збільшити швидкість роботи. У наші дні логіка оптимізації стала дуже складною: компілятори перетворять цикли, умовні вирази і рекурсивні функції; видаляють цілі блоки коду. Вони оптимізують код під програмну архітектуру, щоб зробити його дійсно швидким і компактним. І це дуже добре, адже краще фокусуватися на написання читабельного коду, чим займатися ручними оптимизациями, які буде складно розуміти і підтримувати. Крім того, ручні оптимізації можуть перешкодити компілятору виконати додаткові і більш ефективні автоматичні оптимізації. Замість того, щоб писати оптимізації руками, краще б зосередитися на дизайні архітектури та ефективних алгоритмів, включаючи паралелізм та використання особливостей бібліотек.

Дана стаття присвячена оптимізацій компілятор Visual C++. Я збираюся обговорити найбільш важливі техніки оптимізацій і про рішення, які доводиться застосувати компілятору, щоб правильно їх застосувати. Моя мета не в тому, щоб розповісти вам як вручну оптимізувати код, а в тому, щоб показати чому варто довіряти компілятору оптимізувати ваш код самостійно. Ця стаття ні в якому разі не є описом повного набору оптимізацій, які здійснює компілятор Visual C++, в ній будуть показані тільки дійсно важливі оптимізації, про які корисно знати. Є інші важливі оптимізації, які компілятор виконати не в змозі. Наприклад, заміна неефективного алгоритму на ефективний або зміна вирівнювання структури даних. Такі оптимизациями у цій статті ми обговорювати не будемо.

Визначення оптимізацій компілятораОптимізація — це процес перетворення частини фрагмента коду в інший фрагмент, який функціонально еквівалентний вихідному, з метою поліпшення одного або декількох його характеристик, з яких найбільш важливими є швидкість і розмір коду. Інші характеристики включають кількість споживаної енергії на виконання коду і час компіляції (а також час JIT-компіляції, якщо отриманий код використовує JIT).

Компілятори постійно удосконалюються, використовувані ними підходи поліпшуються. Незважаючи на те, що вони не досконалі, найчастіше найбільш правильним підходом є все-таки залишити низькорівневі оптимізації компілятору, ніж намагатися провести їх вручну.

Існує чотири способи допомогти компілятору провести оптимізацію більш ефективно:
  1. Пишіть читається код, який легко підтримувати. Не думайте про різні ООП-фічі Visual C++, як найлютішому ворогові продуктивності. Остання версія Visual C++ зможе звести накладні витрати від ООП до мінімуму, а іноді навіть повністю від них позбутися.
  2. Використовуйте директиви компілятора. Наприклад, скажіть компілятору використовувати те угоду про виклик функцій, яке буде швидше того, що стоїть за умовчанням.
  3. Використовуйте вбудовані компілятор функції. Це такі спеціальні функції, реалізація яких забезпечується компілятором автоматично. Пам'ятайте, що компілятор володіє глибоким знанням того, як ефективно розташувати послідовність машинних команд так, щоб код працював максимально швидко на зазначеній програмної архітектури. В даний час Microsoft .NET Framework не підтримує вбудовані функції, так що керовані мови не можуть їх використовувати. Однак, Visual C++ має розширену підтримку таких функцій. Втім, не варто забувати про те, що їх використання хоч і поліпшить продуктивність коду, але при цьому негативно позначиться на доступності і портируемости.
  4. Використовуйте profile-guided optimization (PGO). Завдяки цій технології, компілятор знає більше про те, як код буде вести себе під час роботи і оптимізує його відповідному чином.


Мета даної статті полягає в тому, щоб показати вам, чому ви можете довіряти компілятору виконання оптимізацій, які застосовуються до неефективного, але читання коду (перший метод). Також я зроблю короткий огляд profile-guided оптимізацій і згадаю деякі директиви компілятора, які дозволять вам покращити частина вашого вихідного коду.Існує багато технік оптимізацій компілятора починаючи від простих перетворень начебто згортки констант, закінчуючи складними начебто управлінням порядку команд (instruction scheduling). У цій статті ми обмежимося найбільш важливими оптимизациями, які можуть значно поліпшити продуктивність вашого коду (на двухзначное кількість відсотків) і скоротити його розмір шляхом підстановки функцій (function inlining), COMDAT оптимізацій і оптимізацій циклів. Я обговорю перші два підходи в наступному розділі, а потім покажу як можна контролювати виконання оптимізацій в Visual C++. На закінчення, я коротенько розповім про оптимізацію тих, які застосовуються .NET Framework. Протягом всієї статті я буду використовувати Visual Studio 2013 для всіх прикладів.

Link-Time Code GenerationГенерація коду на етапі лінкування (Link-Time Code Generation, LTCG) — це техніка для виконання оптимізацій над всією програмою (whole program optimizations, WPO) для С/С++ коду. Компілятор С/С++ обробляє кожен файл вихідного коду окремо і видає відповідний файл об'єктів (object file). Іншими словами, компілятор може оптимізувати тільки одиночний файл, замість того, щоб оптимізувати всю програму. Однак, деякі важливі оптимізації можуть бути застосовні тільки до програми цілком. Ви можете використовувати ці оптимізації тільки під час лінкування, а не під час компіляції, т. к. програма компонування має повне уявлення про програму.

Якщо LTCG включений (прапор
/GL
), то драйвер компілятора (
cl.exe
) буде викликати тільки front end (
c1.dll
або
c1xx.dll
) і відкладе роботу back end (
c2.dll
) до моменту лінкування. Отримані об'єктні файли містять C Intermediate Language (CIL), а не машинний код. Потім викликається програма компонування (
link.exe
). Він бачить, що об'єктні файли містять CIL-код, і викликає back end, який в свою чергу виконує WPO і генерує бінарні об'єктні файли, щоб програма компонування міг з'єднати їх разом і сформувати виконуваний файл.Front end також виконує деякі оптимізації (наприклад, згортка констант) незалежно від того, чи включені або вимкнені оптимізації. Втім, всі важливі оптимізації виконує back end, і їх можна контролювати за допомогою ключів компіляції.

LTCG дозволяє back end-виконувати багато оптимізації агресивно (з допомогою ключів компілятора
/GL
разом
/O1
або
/O2
та
/Gw
, а також за допомогою ключів зв'язування
/OPT:REF
та
/OPT:ICF
). У даній статті я розповім тільки inlining і COMDAT оптимізації. Повний список LTCG-оптимізацій наведено в документації. Корисно знати, що програма компонування може виконати LTCG над нативними, нативно-керованими і чисто керованими об'єктними файлами, а також над безпечно-керованими (safe managed) об'єктним файлами та safe.netmodules.

Я буду працювати з програмою з двох файлів вихідного коду (
source1.c
та
source2.c
) і заголовочным файлом (
source2.h
). Файли
source1.c
та
source2.c
приведені в лістингу нижче, а заголовковий файл, що містить прототипи всіх функцій
source2.c
, настільки простий, що його приводити я не буду.

// source1.c
#include < stdio.h> // scanf_s and printf.
#include "Source2.h"
int square(int x) { return x*x; }
main() {
int n = 5, m;
scanf_s("%d", &m);
printf("The square of %d is %d.", n, square(n));
printf("The square of %d is %d.", m, square(m));
printf("The cube of %d is %d.", n, cube(n));
printf("The sum of %d is %d.", n, sum(n));
printf("The sum of cubes of %d is %d.", n, sumOfCubes(n));
printf("The %dth prime number is %d.", n, getPrime(n));
}

// source2.c
#include <math.h> // sqrt.
#include <stdbool.h> // bool, true and false.
#include "Source2.h"
int cube(int x) { return x*x*x; }
int sum(int x) {
int result = 0;
for (int i = 1; i < = x; i++) result += i;
return result;
}
int sumOfCubes(int x) {
int result = 0;
for (int i = 1; i < = x; i++) result += cube(i);
return result;
}
static
bool isPrime(int x) {
for (int i = 2; i <= (int)sqrt(x); i++) {
if (x % i == 0) return false;
}
return true;
}
int getPrime(int x) {
int count = 0;
int candidate = 2;
while (count != x) {
if (isPrime(candidate))
++count;
}
return candidate;
}

Файл
source1.c
містить дві функції: функцію
square
, яка обчислює квадрат цілого числа, і головну функцію програми
main
. Головна функція викликає функцію square і всіх функції
source2.c
за винятком
isPrime
. Файл
source2.c
містить 5 функцій:
cube
для зведення цілого числа в третю ступінь, sum для підрахунку суми цілих чисел від 1 до заданого числа,
sumOfCubes
для підрахунку суми кубів цілих чисел від 1 до заданого числа,
isPrime
для перевірки числа на простоту,
getPrime
для отримання простого числа з заданим номером. Я пропустив обробку помилок, оскільки вона не представляє інтересу в даній статті.

Код вийшов дуже простий, але корисний. У нас є кілька функцій, які роблять прості обчислення, деякі з них містять цикли. Функція
getPrime
є найскладнішою, оскільки вона містить цикл
while
, усередині якого викликає функцію
isPrime
, яка також містить цикл. Я буду використовувати цей код для демонстрації однією з важливих оптимізацій компілятора function inlining і декількох додаткових оптимізацій.Розглянемо результат роботи компілятора під трьома різними конфігураціями. Якщо ви будете розбиратися з прикладом самостійно, то вам знадобиться assembler output file (виходить з допомогою ключа компілятора
/FA[s]
) і map file (виходить з допомогою ключа зв'язування
/MAP
) щоб вивчити виконуються COMDAT-оптимізації (програма компонування буде повідомляти про них, якщо ви включите ключі
/verbose:icf
та
/verbose:ref
). Переконайтеся, що всі ключі вказані правильно і продовжуйте читання статті. Я буду використовувати компілятор C (
/TC
), щоб генерується код був простіший для вивчення, але все викладене у статті також застосовується до С++ коду.

Конфігурація DebugКонфігурація Debug використовується головним чином тому, що всі back end оптимізації вимикаються, якщо ви вказуєте ключ
/Od
без ключа
/GL
. У цій конфігурації підсумкові об'єктні файли містить бінарний код, який в точності відповідає вихідного коду. Ви можете вивчити отримані assembler output files і map file, щоб переконатися в цьому. Конфігурація еквівалентна конфігурації Debug в Visual Studio.

Конфігурація Compile-Time Code Generation ReleaseЦя конфігурація схожа на конфігурацію Release (в якій вказані ключі
/O1
,
/O2
або
/Ox
), але вона не включає ключ
/GL
. У цій конфігурації підсумкові об'єктні файли містять оптимізований бінарний код, але при цьому оптимізації рівня всієї програми не виконуються.

Якщо ви подивіться згенерований assembly listing file
source1.c
, то помітите, що здійснилися дві важливі оптимізації. Перший виклик функції
square
,
square(n)
, був замінений на обчислена під час компіляції значення. Як таке сталося? Компілятор помітив, що тіло функції мало, і він вирішив підставити її вміст замість виклику. Потім компілятор звернув увагу на те, що в обчисленні значення присутній локальна змінна n з відомим початковим значенням, яке не змінювалося між початковим присвоюванням і викликом функції. Таким чином, він прийшов до висновку, що можна безпечно обчислити значення операції множення і підставити результат (
25
). Другий виклик функції
square
,
square(m)
, також був заинлайнен, тобто тіло функції було підставлено замість виклику. Але значення змінної m невідомо на момент компіляції, так що компілятор не зміг заздалегідь обчислити значення виразу.

Тепер звернемося до assembly listing file
source2.c
, він набагато більш цікавий. Виклик функції cube функції
sumOfCubes
заинлайнен. Це в свою чергу дало змогу компілятору виконати оптимізацію циклу (про це докладніше в секції «Оптимізації циклів»). У функції
isPrime
були використані інструкції SSE2 для конвертації
int
на
double
при виклику
sqrt
і конвертації
double
на
int
при отриманні результату
sqrt
. За фактом,
sqrt
зголосилася один раз перед початком циклу. Зверніть увагу, що ключ
/arch
вказує компілятору, що x86 використовує SSE2 за замовчуванням (більшість x86-процесорів і x86-64 процесорів підтримують SSE2).

Конфігурація Link-Time Code Generation ReleaseЦя конфігурація ідентична конфігурації Release у Visiual Studio: оптимізації включені і ключ компілятора
/GL
зазначено (ви також можете явно зазначити
/O1
або
/O2
). Тим самим, ми говоримо компілятору формувати об'єктні файли з CIL кодом замість assembly object files. А значить, програма компонування викличе back end компілятора для виконання WPO, як було описано вище. Тепер ми обговоримо кілька WPO оптимізацій, щоб показати величезну користь LTCG. Генеруються assembly code-лістинги для цієї конфігурації доступні online.Поки инлайнинг функцій включений (ключ
/Ob
, який включений, якщо ви ключили оптимізації), ключ
/GL
дозволяє компілятору инлайнить функції, визначені в інших файлах незалежно від ключа
/Gy
(ми обговоримо його трохи пізніше). Ключ зв'язування
/LTCG
є опціональним і впливає тільки на програма компонування.Якщо ви подивитеся на assembly listing file
source1.c
, то ви можете помітити, що всі виклики функцій крім
scanf_s
були заинлайнены. В результаті компілятор зміг виконати обчислення функцій
cube
,
sum
та
sumOfCubes
. Тільки функція
isPrime
не була заинлайнена. Однак, якщо б ви заинлайнили її вручну
getPrime
, то компілятор як виконав би інлайн
getPrime
на
main
.

Як ви можете бачити, инлайнинг функцій — це важливо не тільки з-за того, що оптимізується виклик функцій, але також через те, що він дозволяє компілятору виконати багато додаткові оптимізації. Инлайнинг зазвичай збільшує продуктивність за рахунок збільшення розміру коду. Надмірне використання цієї оптимізації призводить до такого явища, яке називається code bloat. Тому при кожному виклику функції компілятор проводить обчислення витрат і вигод, після чого приймає рішення про те, варто чи инлайнить функцію.

З-за важливості инлайнинга компілятор Visual C++ надає великі можливості по його підтримці. Ви можете сказати компілятору, щоб він ніколи не инлайнил набір функцій з допомогою директиви
auto_inline
. Ви також можете вказати компілятору задані функції або методи з допомогою
__declspec(noinline)
. А ще можна позначити функцію ключовим словом
inline
і порадити компілятору виконати інлайн (хоча, компілятор може вирішити проігнорувати цей рада, якщо вважатиме його поганим). Ключове слово
inline
доступно починаючи з першої версії З++, воно з'явилося в С99. Ви можете використовувати ключове слово
__inline
компілятора від Microsoft і для С і для С++: це зручно, якщо ви хочете використовувати старі версії, які не підтримують дане ключове слово. Ключове слово
__forceinline
(для С++) змушує компілятор завжди инлайнить функцію, якщо це можливо. І останнє, але не за важливістю, ви можете сказати компілятору розгорнути рекурсивні функцію зазначеної або невизначеної глибини шляхом инлайнинга з допомогою директиви
inline_recursion
. Врахуйте, що в даний час компілятор не має можливості контролювати инлайнинг в місці виклику функції, а не в місці її оголошення.

Ключ
/Ob0
вимикає инлайнинг повністю, що корисно під час налагодження (цей ключ спрацьовує в Debug-конфігурації в Visual Studio). Ключ
/Ob1
каже компілятору, що в якості кандидатів на инлайнинг повинні розглядатися тільки функції, позначені за допомогою
inline
,
__inline
,
__forceinline
. Ключ
/Ob2
спрацьовує тільки при зазначених
/O[1/2|x]
і каже компілятору розглядати для инлайнинга всі функції. На мій погляд, єдина причина для використання ключових слів
inline
та
__inline
— контролювання инлайнинга для ключа
/Ob1
.

Компілятор не завжди може заинлайнить функцію. Наприклад, під час віртуального виклику віртуальної функції: функція не може бути заинлайнена, т.к. компілятор не знає точно, яка саме функція буде вызована. Інший приклад: функція викликається через покажчик на функцію замість виклику через її ім'я. Ви повинні намагатися уникати таких ситуацій, щоб инлайнинг був можливий. Повний список усіх таких умов можна знайти в MSDN.

Инлайнинг функцій — це не єдина оптимізація, яка може застосовуватися на рівні програми. Більшість оптимізацій найбільш ефективно працюють саме на цьому рівні. В іншій частині статті, я обговорю специфічний клас оптимізацій, який називається COMDAT-оптимізації.

За промовчанням під час компіляції модуля, весь код зберігається в одиночній секції підсумкового об'єктного файлу. Програма компонування працює на рівні секцій: він може видаляти секції, з'єднувати їх або перевпорядкувати. Це заважає йому виконати три дуже важливих оптимізації (двозначний відсоток), які допомагають зменшити розмір виконуваного файлу і збільшити його продуктивність. Перша видаляє невживані функції та глобальні змінні. Друга згортає ідентичні функції і глобальні константи. Третя змінювати порядок функції та глобальні змінні так, щоб під час виконання переходи між фізичними фрагментами пам'яті були коротше.

Для включення цих оптимізацій зв'язування, ви повинні попросити компілятор упаковувати функції та змінні в окремі секції за допомогою ключів компілятора
/Gy
(лінкування рівня функцій) і
/Gw
(оптимізація глобальних даних). Ці секції називаються COMDAT-ами. Ви також можете позначити задану глобальну змінну з використанням
__declspec( selectany)
, щоб сказати компілятору упаковувати змінну в COMDAT. Далі, з допомогою ключа зв'язування
/OPT:REF
можна позбутися від непотрібних функцій та глобальні змінні. Ключ
/OPT:ICF
допоможе згорнути ідентичні функції і глобальні константи (ICF — це Identical COMDAT Folding). Ключ
/ORDER
змусить програма компонування поміщати COMDAT-и в підсумкові образи в специфічному порядку. Врахуйте, що всі оптимізації зв'язування не потребують ключі
/GL
. Ключі
/OPT:REF
та
/OPT:ICF
повинні бути вимкнені під час налагодження з очевидних причин.

Ви повинні використовувати LTCG завжди, коли це можлива. Єдина причина відмови від LTCG полягає в тому, що ви хочете поширювати підсумкові об'єктні файли і файли бібліотек. Нагадаємо, що вони містять CIL-код замість машинного. CIL-код може бути використаний тільки компілятором і линковщиком тієї ж версії, з допомогою якої вони були згенеровані, що є значним обмеженням, адже розробникам доведеться використовувати ту ж версію компілятора, щоб використовувати ваші файли. В даному випадку, якщо ви не хочете поширювати окрему версію об'єктних файлів для кожної версії компілятора, то ви повинні використовувати замість цього генерацію коду. На додаток до обмеження за версією, об'єктні файли у багато разів більше, ніж відповідні assembler object-файли. Втім, не забувайте про величезну перевагу об'єктних файлів з CIL-кодом, яке полягає в можливості використовувати WPO.

Оптимізації циклівКомпілятор Visual C++ підтримує кілька видів оптимізації циклів, але ми будемо обговорювати тільки три: розмотування циклів (loop unrolling), автоматична векторизація (automatic vectorization) і винос інваріанти циклу (loop-invariant code motion). Якщо ви модифікуєте код
source1.c
так, що
sumOfCubes
буде передаватися m замість n, то компілятор не зможе вирахувати значення параметрів, доведеться компілювати функцію, щоб вона могла працювати для будь-якого аргументу. Підсумкова функція буде добре оптимізована, через що буде мати великий розмір, а значить компілятор не буде її инлайнить.Якщо ви скопмилируете код з ключем
/O1
, то ніякі оптимізації до
sumOfCubes
не застосовується. Компіляція з ключем
/O2
дасть оптимізації швидкості. При цьому розмір коду значно збільшиться, оскільки цикл всередині функції
sumOfCubes
буде размотан і векторизован. Дуже важливо розуміти, що векторизація буде неможлива без инлайнинга функції cube. Більш того, розмотування циклу також не буде настільки ефективно без инлайнинга. Спрощене графічне подання підсумкового коду наведено на наступній картинці (цей граф справедливий як для x86, так і для x86-64).



На даній схемі зелений ромб показує точку входу, а червоні прямокутники показують точки виходу. Блакитні ромби становлять умовні оператори, які будуть виконуватися при виконанні функції
sumOfCubes
. Якщо SSE4 підтримується і x більше або дорівнює 8, то SSE4-інструкції будуть використані для того, щоб виконувати 4 множення за 1 раз. Процес виконання однакової операції декількох змінних називається векторизацией. Також, компілятор двічі размотает цей цикл. Це означає, що тіло цикло буде повторено двічі на кожну ітерацію. В результаті виконання восьми операцій множення буде відбуватися за 1 ітерацію. Якщо x менше 8, то для виконання функції буде використаний код без оптимізацій. Зверніть увагу, що компілятор вставляє три точки виходу замість одного — таким чином зменшується кількість переходів.Розмотування циклів здійснюється повторенням тіла циклу кілька разів всередині однієї ітерації нового (розмотаного) циклу. Це підвищує продуктивність, т. к. операції самого циклу будуть виконуватися рідше. Крім того, це дозволяє компілятору виконувати додаткові оптимізації (наприклад, векторизацію). Недоліком розмотування циклів є збільшення кількості коду і навантаження на регістри. Але незважаючи на це, залежно від тіла циклу подібна оптимізація може збільшити продуктивність на двозначне відсоток.

На відміну від x86-процесорів, всі процесори x86-64 підтримують SSE2. Більш того, ви можете використовувати переваги інструкцій AVX/AVX2 на останніх моделях x86-64 процесорів від Intel і AMD з допомогою ключа
/arch
. Вказавши
/arch:AVX2
, ви скажете компілятор також використовувати інструкції FMA і BMI.

На поточний момент компілятор Visual C++ не дозволяє контролювати розмотування циклів. Але ви можете впливати на неї з допомогою
__forceinline
та директиви
loop
з опцією
no_vector
(остання вимикає автовекторизацию заданих циклів).

Якщо ви подивитеся на згенерований асемблерний код, то можете помітити, що до нього можна застосувати додаткові оптимізації. Але компілятор все-одно вже зробив відмінну роботу і немає потреби витрачати набагато більше часу на аналіз, щоб застосувати мінорні оптимізації.

Функція
someOfCubes
не є єдиною, цикл якої був размотан. Якщо ви модифікуєте код і передасте
m
в функцію
sum
замість
n
, то компілятор не зможе предподсчитать її значення і йому доведеться генерувати код, цикл буде размотан двічі.

У висновку ми подивимося на таку оптимізацію, як винос інваріанти циклу. Погляньте на наступний код:

int sum(int x) {
int result = 0;
int count = 0;
for (int i = 1; i < = x; i++) {
++count;
result += i;
}
printf("%d", count);
return result;
}

Єдина зміна, яку ми зробили, полягає в додаванні додаткової змінної, яка збільшується на кожній ітерації, а в завершенні виводиться на консоль. Не важко помітити, що даний код легко оптимізується за допомогою виносу инкрементируемоей змінної за рамки циклу: досить просто присвоїти їй значення x. Дана оптимізація називається виносом інваріанта циклу (loop-invariant code motion). Слово «інваріант» показує, що ця техніка застосовується тоді, коли частина коду не залежить від виразів, які містять змінну циклу.Але от заковика: якщо ви застосуєте цю оптимізацію вручну, то підсумковий код може втратити в продуктивності в певних умовах. Чи Можете ви сказати чому? Уявіть, що змінна x не позитивна. У цьому випадку цикл буде виконуватися, а в неоптимізованого версії змінна count залишиться недоторканою. Версія соптимизированная вручну виконає зайве присвоювання x в count, яке буде виконано поза циклу! Більш того, якщо x негативно, то змінна count отримає невірне значення. І люди, і компілятора схильні до подібних пасток. На щастя, компілятор Visual C++ досить розумний, щоб здогадатися до цього і перевірити умову циклу до присвоювання, покращуючи продуктивність для всіх можливих значень x.У висновку хочеться сказати, що якщо ви не компілятор і не експерт по оптимізацій компілятора, то ви повинні уникати таких перетворень свого коду, які змусять виглядати так, ніби він працює швидше. Тримайте свої руки чистими і довірте компілятору оптимізувати ваш код.

Контролювання оптимізаційНа додаток до ключів
O1
,
/O2
,
/Ox
, ви можете контролювати специфічні функції з допомогою директиви
optimize
:

#pragma optimize( "[optimization-list]", {on | off} )

В даному прикладі optimization list може бути порожнім, а може містити одне або кілька значень набору:
g
,
s
,
t
,
y
. Вони відповідають ключів
/Og
,
/Os
,
/Ot
,
/Oy
.

Порожній список c параметром off вимикає всі оптимізації незалежно від ключів компілятора. Порожній список з параметром on застосовує вищезазначені ключі компілятора.

Ключ /Og дозволяє виконувати глобальні оптимізації, які можуть бути виконані тільки всередині оптимізованної функції, а не функції, які її викликають. Якщо LTCG включений, то /Og дозволяє робити WPO.

Директива
optimize
дуже корисна у випадках, коли ви хочете, щоб різні функції були оптимізовані різними способами: одні по займаному розміром, а інші по швидкості. Втім, якщо ви дійсно хочете мати контроль оптимізацій такого рівня, ви повинні подивитися в бік profile-guided-оптимізацій(PGO), які являють собою оптимізацію коду з використанням профілю, що зберігає інформацію про поведінку, записану під час виконання інструментальної версії коду. Компілятор використовує профіль для того, щоб забезпечити кращі рішення під час оптимізації коду. Visual Studio представляє спеціальні інструменти, щоб застосувати цю техніку як до нативному, так і до керованого коду.

Оптимізації .NET.NET немає зв'язування, який був би втягнений в модель компіляції. Замість цього є компілятор вихідного коду (C# компілятор) і JIT-компілятор. Над вихідним кодом виконуються тільки мінорні оптимізації. Наприклад, на цьому рівні ви не побачите инлайнинга функції або оптимізацію циклів. Замість цього дані оптимізації виконуються на рівні JIT-компіляції. JIT-компілятор до версії .NET 4.5 не підтримував SIMD інструкцій. А ось JIT-компілятор з версії .NET 4.5.1 (який називається RyuJIT) підтримує SIMD.

У чому ж різниця між RyuJIT і Visual C++ з точки зору можливостей оптимізації? Зважаючи на те, що RyuJIT працює під час виконання, він може виконати такі оптимізації, на які Visual C++ не здатний. Наприклад, прямо під час виконання він може зрозуміти, що вираз в умовному операторі ніколи не прийме значення true в поточній запущеної версії додаток, а потім застосувати відповідні оптимізації. Також RyuJIT може використовувати знання про використовуваної процесорної архітектури під час виконання. Наприклад, якщо процесор підтримує SSE4.1, то JIT компілятор використовує тільки інструкцій SSE4.1 для реалізації функції subOfCubes, що дозволить зробити генерований код більш компактним. Але потрібно розуміти, що RyuJIT не може витрачати на оптимізації багато часу, оскільки час JIT-компіляції впливає на продуктивність програми. А ось компілятор Visual C++ може витратити на аналіз коду багато часу, щоб знайти можливості оптимізації для поліпшення підсумкового виконуваного файлу. Завдяки новій чудовій технології від Microsoft під назвою .NET Native ви можете компілювати керований код в автономні виконувані файли з використанням оптимізацій Visual C++. В даний час ця технологія підтримує тільки додатки Windows Store.

На сьогоднішній день можливість контролювати оптимізації керованого коду обмежена. Компілятори C# і Visual Basic дозволяють тільки включати або вимикати оптимізації з допомогою ключа
/optimize
. Для контролю JIT-оптимізацій, ви можете застосувати атрибут
System.Runtime.CompilerServices.MethodImpl
до потрібного методу з опцією з
MethodImplOptions
. Опція
NoOptimization
вимикає оптимізації, опція
NoInlining
забороняє инлайнить метод, а опція
AggressiveInlining
(доступна с .NET 4.5) дає JIT-компілятора рекомендацію, що йому слід заинлайнить даний метод.

ПідсумокВсі методи оптимізації, які обговорювалися в цій статті, можуть значно поліпшити продуктивність вашого коду на двозначне відсоток. Всі вони підтримуються в компіляторі Visual C++. Що робить ці техніки дійсно важливими, так це те, що після їх застосування компілятор здатний робити інші оптимізації. Ця стаття ні в якому разі не є повним обговоренням всіх оптимізацій, які виконує Visual C++. Тим не менш, я сподіваюся, що тепер ви цінуєте можливості компілятора. Visual C++ може набагато, набагато більше. Більш докладно ми поговоримо про це в частині 2.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.