Наши партнеры

UnixForum





Библиотека сайта rus-linux.net

Ninja

Глава 3 из книги "Производительность приложений с открытым исходным кодом".

Оригинал: Ninja
Автор: Evan Martin
Перевод: А.Панин

Файлы зависимостей

Система сборки проектов должна предусматривать реализацию отдельного хранилища для метаданных, которые должны записываться и использоваться в ходе повторных сборок. Для корректной сборки кода на языках программирования C/C++ система сборки должна разрешать зависимости между заголовочными файлами. Предположим, что файл исходного кода foo.c содержит строку #include "bar.h", а заголовочный файл bar.h, в свою очередь, содержит строку #include "baz.h". После этого все три описанных файла (foo.c, bar.h, baz.h) будут оказывать влияние на результат компиляции. Например, изменения в файле baz.h должны будут приводить к повторной генерации объектного файла foo.o.

Некоторые системы сборки проектов используют "сканер заголовочных файлов" для извлечения информации об этих зависимостях в процессе сборки, но данный подход может оказаться медленным и достаточно сложно реализуемым в случае присутствия в файлах директив #ifdef. Альтернативой данному подходу является требование к файлам сборки о предоставлении отчета обо всех зависимостях, включая заголовочные файлы, но данное требование затрудняет деятельность разработчиков: каждый раз, когда вы добавляете или удаляете объявление #include, вам придется модифицировать или повторно создавать файл сборки.

Более удачный подход опирается на тот факт, что в процессе компиляции компилятор gcc (и компилятор компании Microsoft из состава Visual Studio) может выводить информацию о том, какие заголовочные файлы были использованы для сборки выходного файла. Эта информация, как и команда, использованная для генерации выходного файла, может быть записана и повторно загружена системой сборки, поэтому зависимости могут надежно отслеживаться. При первой сборке перед появлением каких-либо выходных данных все файлы будут скомпилированы, поэтому информация о зависимостях заголовочных файлов не является необходимой. После первой компиляции модификации любых файлов, использованных для генерации выходного файла (включая модификации, в ходе которых добавляются или удаляются дополнительные зависимости) приведут к пересборке с поддержанием информации о зависимостях в актуальном состоянии.

В процессе компиляции компилятор gcc записывает информацию о зависимостях заголовочных файлов в формате, используемом в файлах Makefile. Впоследствии Ninja подключает систему разбора для синтаксических конструкций файлов Makefile (точнее их упрощенного подмножества) и загружает всю информацию о зависимостях в процессе следующей сборки. Процесс загрузки этих данных является основным узким местом в плане производительности. В последних сборках Chrome общий объем генерируемой gcc информации о зависимостях заголовков в форме файлов Makefile достигает 90 МБ, причем в каждом из этих файлов используются относительные пути, которые должны быть канонизированы.

Как и при выполнении другой работы по разбору данных, использование генератора лексических анализаторов re2c и удаление копий данных во всех возможных местах позволяют повысить производительность. Однако, по аналогии с тем, как работа переносится на программный компонент GYP, описанная работа по разбору данных может быть перенесена на момент времени, который не будет связан с процессом запуска системы. Наше последнее мероприятие по усовершенствованию системы Ninja (на момент написания данной главы, эта возможность была реализована, но не была включена в релиз) направлено на ускорение этого процесса обработки данных в ходе сборки.

После того, как система Ninja начала выполнять команды сборки, вся критичная к производительности работа завершается и Ninja практически находится в бездействии, ожидая завершения выполнения команд, которые должны быть выполнены для завершения сборки. При использовании усовершенствованного подхода к обработке зависимостей заголовочных файлов, Ninja тратит это время на обработку файлов Makefile по мере их записи после генерации средствами gcc, выполняя канонизацию путей и осуществляя преобразование данных файлов зависимостей с использованием подвергающегося быстрому разбору бинарного формата. Благодаря данной оптимизации мы добились огромного повышения производительности, особенно на платформе Windows. (Этот вопрос будет обсуждаться позднее в данной главе.)

"Журнал зависимостей" должен хранить тысячи путей и зависимостей между этими путями. Операции загрузки этого журнала и добавления в него данных должны выполняться быстро. Добавление данных в этот журнал должно осуществляться безопасно, даже при наступлении события прерывания сборки, вызванного таким обстоятельством, как ее отмена.

После рассмотрения множества аналогичных базам данных подходов, я в конце концов выбрал тривиальную реализацию: файл представляет собой последовательность записей и каждая запись является либо путем, либо списком зависимостей. Каждому записываемому в файл пути ставится в соответствие последовательный целочисленный идентификатор. В этом случае зависимости представляются в виде списков целочисленных значений. При добавлении зависимостей в файл Ninja в первую очередь добавляет новые записи для каждого пути, которому пока не поставлен в соответствие идентификатор, после чего добавляет запись, содержащую список зависимостей и использующую эти идентификаторы. Таким образом, при загрузке файла в ходе последующего запуска Ninja может использовать простой массив для сопоставления идентификаторов с указателями на структуры Node соответствующих им записей.

Выполнение сборки

С точки зрения производительности процесс выполнения команд сборки, организуемый в соответствии с описанными выше зависимостями, является относительно не интересным, так как основная часть необходимой работы выполняется с помощью данных команд (т.е., средствами компиляторов, линковщиков, и.т.д.), а не с помощью самой системы Ninja.10

По умолчанию Ninja выполняет команды сборки в параллельном режиме, распределяя задачи в соответствии с количеством центральных процессоров, доступных в системе. Так как вывод выполняющихся одновременно команд может смешиваться, Ninja буферизует всю выводимую в процессе выполнения команды информацию до момента завершения ее выполнения, после которого осуществляет непосредственный вывод накопленной информации. В результате информация выводится так же, как и в том случае, если бы команды исполнялись последовательно.11

Этот контроль над выводом команд позволяет Ninja тщательно контролировать свой вывод. В процессе сборки Ninja выводит единственную строку с информацией о состоянии при обработке данных; в случае, если сборка завершается успешно вся выводимая Ninja информация также ограничивается одной строкой.12 Это обстоятельство не позволяет Ninja работать быстрее, но оно позволяет создать иллюзию быстрой работы, которая важна почти также, как и первоначальная цель, заключающаяся в повышении реальной скорости работы.

Поддержка платформы Windows

Я разрабатывал версию Ninja для Linux. Nico (о котором упоминалось ранее) выполнял работу по приведению ее в работоспособное состояние на платформе Mac OS X. По мере того, как круг пользователей системы Ninja расширялся, люди начали задавать вопросы по поводу поддержки платформы Windows.

Обеспечение поддержки платформы Windows на базовом уровне не было достаточно сложной задачей. В ходе реализации поддержки были внесены такие очевидные изменения, как добавление поддержки обратного слэша в качестве разделителя путей или изменение синтаксиса Ninja для поддержки двоеточия в строках путей (таких, как c:\foo.txt). После внесения этих изменений мы столкнулись с более значительными проблемами. Система Ninja была спроектирована с учетом особенностей функционирования платформы Linux; платформа Windows имеет небольшие, но важные отличия.

Например, Windows устанавливает сравнительно малую максимальную длину команды, что приводит к появлению проблем в ходе формирования команды, которая передается на завершающем этапе связывания и должна включать имена большей части файлов проекта. Решение, примененное на платформе Windows, заключается в использовании файлов "ответа", с которыми имеет возможность работать исключительно система Ninja (а не программа-генератор, работающая с Ninja).

Более важная проблема с производительностью заключалась в том, что операции с файлами в Windows выполнялись медленно, а системе Ninja приходилось работать множеством файлов. Компилятор из комплекта поставки Visual Studio генерирует данные о зависимостях заголовков, просто выводя информацию о них в процессе компиляции, поэтому версия Ninja для Windows на данный момент включает в свой комплект поставки инструмент, который взаимодействует напрямую с компилятором для того, чтобы последний выводил списки зависимостей в формате генерируемых gcc файлов Makefile, которые требуются Ninja. Это большое количество файлов, уже являющееся причиной снижения производительности в Linux, приводит к значительно худшим последствиям в Windows, где операция открытия файла является значительно более ресурсоемкой. Ранее описанный новый подход к разбору зависимостей в процессе компиляции отлично подходит к условиям работы с Windows, позволяя нам полностью избавится от промежуточного инструмента: система Ninja уже буферизует выводимую в ходе исполнения команды информацию, поэтому она имеет возможность производить разбор данных зависимостей непосредственно в буфере, обходя промежуточные хранящиеся на диске файлы Makefile, используемые при работе с gcc.

Скорость работы функции получения времени модификации файла - GetFileAttributesEx() на платформе Windows13 значительно отличается от скорости работы функции stat() на всех не относящихся к Windows платформах - по нашим наблюдениям в Windows скорость работы функции примерно в 100 раз ниже, чем в Linux.14

Возможно, это происходит из-за таких факторов, приводящих к "неравным" условиям, как работа антивирусного программного обеспечения, но на практике эти факторы присутствуют и в системах конечных пользователей, поэтому производительность системы Ninja снижается в любом случае. Система контроля версий Git, которой также требуется получать данные состояния множества файлов, может использовать множество потоков при работе на платформе Windows для выполнения проверок файлов в параллельном режиме. Системе Ninja следовало бы перенять такую возможность.


Продолжение статьи: Выводы и альтернативные архитектуры.