Я працюю над проектом STAPL, який є широко розробленою C ++ бібліотекою. Час від часу нам доводиться переглянути всі методики для скорочення часу на компіляцію. Тут я узагальнив методи, якими ми користуємось. Деякі з цих методів уже перераховані вище:
Пошук найбільш трудомістких розділів
Хоча немає підтвердженої кореляції між довжиною символу та часом компіляції, ми помітили, що менші середні розміри символів можуть покращити час компіляції для всіх компіляторів. Тож ваші перші цілі - знайти найбільші символи у вашому коді.
Спосіб 1 - Сортування символів залежно від розміру
За допомогою nm
команди можна перераховувати символи виходячи з їх розмірів:
nm --print-size --size-sort --radix=d YOUR_BINARY
У цій команді --radix=d
ви можете бачити розміри в десяткових числах (за замовчуванням - шістнадцятковий). Тепер, подивившись на найбільший символ, визначте, чи можете ви зламати відповідний клас та спробуйте його переробити, розбивши фактори, що не є шаблонами, в базовий клас або розділивши клас на кілька класів.
Спосіб 2 - Сортування символів на основі довжини
Ви можете запустити звичайну nm
команду і передати її улюбленому сценарію ( AWK , Python тощо), щоб сортувати символи залежно від їх довжини . На основі нашого досвіду цей метод визначає найбільшу проблему з тим, щоб зробити кандидатів кращим, ніж метод 1.
Спосіб 3 - Використовуйте Templight
" Templight - це засіб на базі Кланг для профілактики часу та споживання пам'яті шаблонів шаблонів та проведення інтерактивних сесій налагодження для отримання інтроспекції процесу інстанції шаблону".
Ви можете встановити Templight, перевіривши LLVM та Clang ( інструкції ) та застосувавши на ньому патч Templight. Налаштування LLVM та Clang за замовчуванням є налагодженням і твердженнями, і це може значно вплинути на час вашої компіляції. Здається, що Templight потребує обох, тому вам доведеться використовувати налаштування за замовчуванням. Процес установки LLVM та Clang повинен тривати близько години.
Після застосування патча ви можете використовувати templight++
розташований у папці збірки, яку ви вказали при встановленні для складання вашого коду.
Переконайтесь, що templight++
це у вашій ПАРТІ. Тепер для компіляції додайте такі перемикачі до своїх CXXFLAGS
у вашому Makefile або до параметрів командного рядка:
CXXFLAGS+=-Xtemplight -profiler -Xtemplight -memory -Xtemplight -ignore-system
Або
templight++ -Xtemplight -profiler -Xtemplight -memory -Xtemplight -ignore-system
Після компіляції у вас буде створено .trace.memory.pbf та .trace.pbf, створені в одній папці. Для візуалізації цих слідів можна скористатися інструментами Templight, які можуть конвертувати їх в інші формати. Дотримуйтесь цих інструкцій, щоб встановити templight-convert. Зазвичай ми використовуємо висновок callgrind. Ви також можете використовувати висновок GraphViz, якщо ваш проект невеликий:
$ templight-convert --format callgrind YOUR_BINARY --output YOUR_BINARY.trace
$ templight-convert --format graphviz YOUR_BINARY --output YOUR_BINARY.dot
Згенерований файл callgrind можна відкрити за допомогою kcachegrind, в якому ви зможете простежити найбільше часу / пам’яті, що вимагає часу.
Зменшення кількості екземплярів шаблону
Хоча не існує точного рішення щодо зменшення кількості екземплярів шаблонів, є кілька вказівок, які можуть допомогти:
Класи Refactor з більш ніж одним аргументом шаблону
Наприклад, якщо у вас є клас,
template <typename T, typename U>
struct foo { };
і обидва, T
і U
можуть мати 10 різних варіантів, ви збільшили можливі екземпляри шаблону цього класу до 100. Один із способів вирішити це - абстрагувати загальну частину коду до іншого класу. Інший метод полягає у використанні інверсії успадкування (повернення ієрархії класів), але переконайтесь, що ваші цілі проектування не порушені перед використанням цієї методики.
Не шаблонований код Refactor на окремі одиниці перекладу
Використовуючи цю методику, ви можете скласти загальний розділ один раз і зв'язати його згодом з іншими TU (перекладацькими одиницями).
Використовуйте екземпляри шаблону зовнішнього шаблону (оскільки C ++ 11)
Якщо ви знаєте всі можливі інстанції класу, ви можете скористатися цією технікою для складання всіх справ у іншому підрозділі перекладу.
Наприклад, у:
enum class PossibleChoices = {Option1, Option2, Option3}
template <PossibleChoices pc>
struct foo { };
Ми знаємо, що цей клас може мати три можливі моменти:
template class foo<PossibleChoices::Option1>;
template class foo<PossibleChoices::Option2>;
template class foo<PossibleChoices::Option3>;
Покладіть вище в одиницю перекладу і використовуйте ключове слово extern у файлі заголовка, нижче визначення класу:
extern template class foo<PossibleChoices::Option1>;
extern template class foo<PossibleChoices::Option2>;
extern template class foo<PossibleChoices::Option3>;
Ця методика може заощадити ваш час, якщо ви збираєте різні тести із загальним набором даних.
ПРИМІТКА: MPICH2 ігнорує явну інстанцію в цей момент і завжди збирає інстанційовані класи у всіх підрозділах компіляції.
Використовуйте побудови єдності
Вся ідея побудови єдності полягає в тому, щоб включити всі .cc файли, які ви використовуєте в одному файлі, і компілювати цей файл лише один раз. Використовуючи цей метод, ви можете уникнути відновлення загальних розділів різних файлів, і якщо ваш проект включає багато загальних файлів, ви, ймовірно, також заощадите на доступі до диска.
В якості прикладу, давайте припустимо , що у вас є три файли foo1.cc
, foo2.cc
, foo3.cc
і всі вони включають в себе tuple
від STL . Ви можете створити такий, foo-all.cc
який виглядає так:
#include "foo1.cc"
#include "foo2.cc"
#include "foo3.cc"
Ви збираєте цей файл лише один раз і, можливо, зменшите загальні екземпляри між трьома файлами. Загалом важко передбачити, чи може покращення бути значним чи ні. Але одним очевидним фактом є те, що ви втратите паралелізм у своїх збірках (ви не можете більше збирати три файли одночасно).
Крім того, якщо якийсь із цих файлів займає багато пам’яті, можливо, у вас фактично не вистачить пам’яті до завершення компіляції. У деяких компіляторах, таких як GCC , це може ICE (Internal Compiler Error) вашого компілятора через відсутність пам'яті. Тому не використовуйте цю техніку, якщо ви не знаєте всіх плюсів і мінусів.
Заздалегідь складені заголовки
Попередньо складені заголовки (PCH) можуть заощадити багато часу на компіляції, склавши файли заголовків до проміжного представлення, розпізнаваного компілятором. Щоб генерувати заздалегідь складені файли заголовків, вам потрібно лише скомпілювати файл заголовка за допомогою звичайної команди компіляції. Наприклад, на GCC:
$ g++ YOUR_HEADER.hpp
Це призведе до генерування YOUR_HEADER.hpp.gch file
( .gch
розширення для PCH-файлів у GCC) у тій же папці. Це означає, що якщо ви включите YOUR_HEADER.hpp
у якийсь інший файл, компілятор раніше використовуватиме ваш YOUR_HEADER.hpp.gch
замість YOUR_HEADER.hpp
цього ж папки.
Існує два питання щодо цієї методики:
- Ви повинні переконатися, що попередньо складені файли заголовка є стабільними та не зміняться ( ви завжди можете змінити свій файл файлів )
- Ви можете включити лише одну PCH на одиницю компіляції (для більшості компіляторів). Це означає, що якщо у вас є декілька файлів заголовка, які потрібно попередньо скласти, ви повинні включити їх до одного файлу (наприклад,
all-my-headers.hpp
). Але це означає, що вам доведеться включати новий файл у всі місця. На щастя, GCC має рішення для цієї проблеми. Використовуйте -include
і дайте йому новий файл заголовка. За допомогою цієї методики ви можете розділяти комами різні файли.
Наприклад:
g++ foo.cc -include all-my-headers.hpp
Використовуйте неназвані або анонімні простори імен
Неназвані простори імен (також анонімні простори імен) можуть значно зменшити створені двійкові розміри. У безіменних просторах імен використовуються внутрішні зв’язки, тобто символи, згенеровані в цих просторах імен, не будуть видимі іншим TU (одиницям перекладу чи компіляції). Компілятори зазвичай генерують унікальні імена для безіменних просторів імен. Це означає, що якщо у вас є файл foo.hpp:
namespace {
template <typename T>
struct foo { };
} // Anonymous namespace
using A = foo<int>;
І вам трапляється включати цей файл у два TU (два .cc файли та компілювати їх окремо). Два екземпляри шаблона foo не будуть однаковими. Це порушує правило одного визначення (ODR). З цієї ж причини використання безіменних просторів імен не рекомендується у файлах заголовків. Сміливо використовуйте їх у своїх .cc
файлах, щоб уникнути появи символів у ваших бінарних файлах. У деяких випадках зміна всіх внутрішніх реквізитів .cc
файлу показала 10% зменшення створених двійкових розмірів.
Зміна параметрів видимості
У новіших компіляторах ви можете вибрати, щоб ваші символи були видимими або невидимими в динамічних спільних об'єктах (DSO). В ідеалі зміна видимості може покращити продуктивність компілятора, оптимізацію часу зв'язку (LTO) та генеровані двійкові розміри. Якщо ви подивитеся на файли заголовків STL у GCC, то можна побачити, що він широко використовується. Щоб увімкнути вибір видимості, вам потрібно змінити код на функцію, на клас, на змінну і, що ще важливіше, на компілятор.
За допомогою наочності ви можете приховати символи, які вважаєте їх приватними, від створених спільних об'єктів. У GCC ви можете контролювати видимість символів, перейшовши за замовчуванням або приховавши -visibility
параметр вашого компілятора. Це певним чином схоже з простою назвою імен, але більш детально та нав'язливо.
Якщо ви хочете вказати видимість для кожного випадку, вам слід додати наступні атрибути до своїх функцій, змінних та класів:
__attribute__((visibility("default"))) void foo1() { }
__attribute__((visibility("hidden"))) void foo2() { }
__attribute__((visibility("hidden"))) class foo3 { };
void foo4() { }
Видимість за замовчуванням у GCC є типовою (загальнодоступною), тобто якщо ви компілюєте вище як -shared
метод спільної бібліотеки ( ), foo2
а клас foo3
не буде видно в інших TU ( foo1
і foo4
буде видно). Якщо ви компілюєте, -visibility=hidden
тоді foo1
буде видно лише Навіть foo4
було б приховано.
Детальніше про видимість ви можете прочитати на вікі GCC .