Я работаю над проектом STAPL, который представляет собой библиотеку C ++ с большим количеством шаблонов. Время от времени мы должны пересматривать все методы, чтобы сократить время компиляции. Здесь я кратко изложил методы, которые мы используем. Некоторые из этих методов уже перечислены выше:
Поиск наиболее трудоемких разделов
Хотя нет доказанной корреляции между длиной символов и временем компиляции, мы заметили, что меньшие средние размеры символов могут улучшить время компиляции на всех компиляторах. Итак, ваши первые цели - найти самые большие символы в вашем коде.
Метод 1 - сортировка символов по размеру
Вы можете использовать nm
команду для вывода списка символов на основе их размеров:
nm --print-size --size-sort --radix=d YOUR_BINARY
В этой команде --radix=d
вы можете увидеть размеры в десятичных числах (по умолчанию шестнадцатеричное). Теперь, взглянув на самый большой символ, определите, можете ли вы разбить соответствующий класс, и попытайтесь изменить его, разложив не шаблонные части в базовом классе или разделив класс на несколько классов.
Метод 2 - сортировка символов по длине
Вы можете запустить обычную nm
команду и направить ее в ваш любимый скрипт ( AWK , Python и т. Д.), Чтобы отсортировать символы по их длине . Основываясь на нашем опыте, этот метод определяет самые большие проблемы, делая кандидатов лучше, чем метод 1.
Способ 3 - использовать Templight
« Templight - это инструмент, основанный на Clang, который позволяет профилировать время и потребление памяти при создании экземпляров шаблонов и выполнять интерактивные сеансы отладки, чтобы получить интроспекцию процесса создания шаблонов».
Вы можете установить 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, в котором вы можете отследить наиболее инстанцирование, которое занимает больше времени / памяти.
Сокращение количества шаблонов
Хотя нет точного решения для сокращения количества экземпляров шаблона, есть несколько рекомендаций, которые могут помочь:
Рефакторинг классов с более чем одним аргументом шаблона
Например, если у вас есть класс,
template <typename T, typename U>
struct foo { };
и оба из T
и U
могут иметь 10 различных опций, вы увеличили возможные экземпляры шаблонов этого класса до 100. Один из способов решить эту проблему - абстрагировать общую часть кода в другой класс. Другой метод заключается в использовании инверсии наследования (реверсирование иерархии классов), но перед использованием этого метода убедитесь, что ваши цели проектирования не поставлены под угрозу.
Рефакторинг не шаблонного кода для отдельных единиц перевода
Используя эту технику, вы можете один раз скомпилировать общий раздел и позже связать его с другими вашими 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 игнорирует явное создание экземпляров в этой точке и всегда компилирует созданные экземпляры классов во всех единицах компиляции.
Используйте единство
Основная идея Unity builds состоит в том, чтобы включить все файлы .cc, которые вы используете, в один файл и скомпилировать этот файл только один раз. Используя этот метод, вы можете избежать повторного создания общих разделов различных файлов, и если ваш проект содержит много общих файлов, вы, вероятно, также сэкономите на доступе к диску.
В качестве примера, давайте предположим , что у вас есть три файла foo1.cc
, foo2.cc
, foo3.cc
и все они включают в себя tuple
от STL . Вы можете создать foo-all.cc
что выглядит так:
#include "foo1.cc"
#include "foo2.cc"
#include "foo3.cc"
Вы компилируете этот файл только один раз и потенциально уменьшаете общие экземпляры среди трех файлов. Трудно вообще предсказать, может ли улучшение быть значительным или нет. Но одним очевидным фактом является то, что вы потеряете параллелизм в ваших сборках (вы больше не сможете компилировать три файла одновременно).
Кроме того, если какой-либо из этих файлов занимает много памяти, вам может фактически не хватить памяти до завершения компиляции. На некоторых компиляторах, таких как GCC , это может привести к ICE (внутренней ошибке компилятора) вашего компилятора из-за нехватки памяти. Так что не используйте эту технику, если вы не знаете все плюсы и минусы.
Предварительно скомпилированные заголовки
Предварительно скомпилированные заголовки (PCH) могут сэкономить вам много времени при компиляции, скомпилировав заголовочные файлы в промежуточное представление, распознаваемое компилятором. Чтобы сгенерировать предварительно скомпилированные файлы заголовков, вам нужно только скомпилировать файл заголовка с помощью обычной команды компиляции. Например, на GCC:
$ g++ YOUR_HEADER.hpp
Это создаст YOUR_HEADER.hpp.gch file
( .gch
это расширение для файлов PCH в GCC) в той же папке. Это означает, что если вы включите YOUR_HEADER.hpp
в какой-то другой файл, компилятор будет использовать ваш YOUR_HEADER.hpp.gch
вместо YOUR_HEADER.hpp
той же папки ранее.
Есть две проблемы с этой техникой:
- Вы должны убедиться, что прекомпилированные файлы заголовков стабильны и не будут меняться ( вы всегда можете изменить свой make-файл )
- Вы можете включить только один 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 - это default (public), что означает, что если вы скомпилируете вышеупомянутое как -shared
метод shared library ( ), foo2
и класс foo3
не будет виден в других TU ( foo1
и foo4
будет виден). Если вы скомпилируете -visibility=hidden
то только foo1
будет видно. Даже foo4
будет скрыт.
Вы можете прочитать больше о видимости на вики GCC .