Я работаю над проектом 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++
находится в вашей переменной PATH. Теперь для компиляции добавьте следующие ключи к вашему CXXFLAGS
в вашем Makefile или к параметрам командной строки:
CXXFLAGS+=-Xtemplight -profiler -Xtemplight -memory -Xtemplight -ignore-system
Или
templight++ -Xtemplight -profiler -Xtemplight -memory -Xtemplight -ignore-system
После завершения компиляции вы получите файлы .trace.memory.pbf и .trace.pbf, созданные в одной папке. Чтобы визуализировать эти следы, вы можете использовать Templight Tools , которые могут преобразовывать их в другие форматы. Следуйте этим инструкциям , чтобы установить 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 не будут одинаковыми. Это нарушает правило One Definition (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 (общедоступная), что означает, что если вы скомпилируете вышеупомянутый метод с использованием общей библиотеки (-shared
), foo2
и класс foo3
не будут видны в других TU (foo1
и foo4
будут видны). Если вы скомпилируете с -visibility=hidden
, тогда будет виден только foo1
. Даже foo4
будет скрыто.
Подробнее о видимости можно прочитать на вики GCC .