Как компилятор замеряет скорость компиляции? 🕰
В C++ легко сделать так, чтобы проект собирался очень долго. В моем личном топе - юнит-тест (в виде одного
.cpp-файла) компилировался 4.5 минуты.К счастью, скорость компиляции можно дебажить. Во время компиляции одного файла нужно указать настройку -ftime-trace:
-ftime-traceКоманда может выглядеть так:
Turn on time profiler. Generates JSON file based on output filename. Results can be analyzed with chrome://tracing or Speedscope App for flamegraph visualization.
-ftime-trace-granularity=<arg>
Minimum time granularity (in microseconds) traced by time profiler
clang++ main.cpp -c -ftime-trace -ftime-trace-granularity=50Получившийся json-файл
main.json можно визуализовать на 🔬SpeedScope. (На гитхабе есть гифка, как это примерно выглядит)Что делает компилятор:
⚙️ Перед началом компиляции, если задана настройка
-ftime-trace, clang вызовет метод llvm::timeTraceProfilerInitialize.⚙️ В этом методе проинициализируется объект структуры llvm::TimeTraceProfiler.
⚙️ Когда начинается какое-то событие, нужно вызвать метод llvm::TimeTraceProfiler::begin, чтобы запомнить время начала.
⚙️ Когда событие заканчивается, нужно вызвать метод llvm::TimeTraceProfiler::end, чтобы добавить запись о событии.
⚙️ Как видно по коду, используется стек, потому что события вложены друг в друга (например внутри события "компиляция файла" есть событие "распарсить класс").
⚙️ После компиляции файла вызывается метод llvm::TimeTraceProfiler::write для записи в json-файл.
По умолчанию параметр
-ftime-trace-granularity равен 500 (500 микросекунд). Записываются не все события, а только достаточно "долгие", которые длились дольше чем 500μs - участок кода.В коде нужные методы не вызывают "вручную" - используется стандартная идиома RAII в виде структуры llvm::TimeTraceScope.
Как видно, в момент вызова конструктора "событие начинается", вызова деструктора "событие заканчивается".
(если компиляция вызывалась без флага
-ftime-trace, то этот объект не делает ничего)Можно привести примеры - вот так засекается время на инстанциацию шаблонов (которая происходит после парсинга файла): PerformPendingInstantiations.
Пока происходит инстанциация шаблонов, засекаются всякие "вложенные" события, например InstantiateFunction.
Вот так компилятор нехитрым образом делает нужный flame graph 🙂
По моему опыту наблюдений за скоростью компиляции, "фронтенд" компилятора (парсинг файла в AST) занимает в 3-20 раз больше времени чем "бэкенд" (перевод AST в LLVM IR, оптимизация и перевод в бинарник).
Основная причина этого дисбаланса - огромный объем исходного файла после того, как раскроются все
#include (в почти всех современных проектах на C++).На основе этих данных становится видно, что нужно поправить, чтобы ускорить компиляцию. А впрочем, это уже совсем другая история...