Хабрахабр

Как мы перевели 10 миллионов строк кода C++ на стандарт C++14 (а потом и на C++17)

Некоторое время назад (осенью 2016), при разработке очередной версии технологической платформы 1С:Предприятие внутри команды разработки встал вопрос о поддержке нового стандарта C++14 в нашем коде. Переход на новый стандарт, как мы предполагали, позволил бы нам писать многие вещи элегантней, проще и надежней, упрощал поддержку и сопровождение кода. И в переводе вроде бы нет ничего экстраординарного, если бы не масштабы кодовой базы и специфические особенности нашего кода.

В общих чертах в состав продукта входят: Для тех кто не знает, 1С:Предприятие – это среда для быстрой разработки кросс-платформенных бизнес-приложений и runtime для их выполнения в разных ОС и СУБД.

  • Кластер серверов приложений, работает на Windows и Linux
  • Клиент, работающий с сервером по http(s) или по собственному бинарному протоколу, работает на Windows, Linux, macOS
  • Среда разработки (Конфигуратор), работает на Windows, Linux, macOS
  • Инструменты администрирования серверов приложений, работают на Windows, Linux, macOS
  • Мобильный клиент, подключающийся к серверу по http(s), работает на мобильных устройствах под управлением Android, iOS, Windows
  • Мобильная платформа — фреймворк для создания оффлайновых мобильных приложений с возможностью синхронизации, работающих на мобильных устройствах под управлением Android, iOS, Windows
  • Среда разработки 1C:Enterprise Development Tools, написана на Java

Мы стараемся по максимуму писать один код для разных ОС — кодовая база сервера общая на 99%, клиента — примерно на 95%. Технологическая платформа 1С:Предприятия преимущественно написана на C++ и ниже приведены приблизительные характеристики кода:

  • 10 миллионов строк С++ кода,
  • 14 тысяч файлов,
  • 60 тысяч классов,
  • полмиллиона методов.

И все это хозяйство надо было перевести на C++14. О том, как мы это делали и с чем столкнулись в процессе, мы сегодня и расскажем.

image

Дисклеймер

Все написанное ниже о медленной/быстрой работе, (не)большом потреблении памяти реализациями стандартных классов в различных библиотеках означает одно: это справедливо ДЛЯ НАС. Вполне возможно, для ваших задач стандартные реализации подойдут наилучшим образом. Мы же отталкивались от своих задач: брали типичные для наших клиентов данные, прогоняли на них типичные сценарии, смотрели на быстродействие, объем потребляемой памяти и т.п., и анализировали – устраивают ли нас и наших клиентов такие результаты или нет. И поступали в зависимости от.

Что у нас было

Изначально мы писали код платформы 1С:Предприятие 8 на Microsoft Visual Studio. Проект начался в начале 2000-х и у нас была версия только под Windows. Естественно, с тех пор код активно развивался, многие механизмы были полностью переписаны. Но код писался по стандарту 1998 года, и правые угловые скобки у нас были разделены пробелами, чтобы успешно проходила компиляция, вот так:

vector<vector<int> > IntV;

В 2006 году, с выходом версии платформы 8.1, мы начали поддерживать Linux и перешли на стороннюю стандартную библиотеку STLPort. Одной из причин перехода была работа с широкими строками. В нашем коде мы повсеместно используем std::wstring, основанный на типе wchar_t. Его размер в Windows 2 байта, а в Linux по умолчанию 4 байта. Это приводило к несовместимости наших бинарных протоколов между клиентом и сервером, а также различных персистентных данных. Опциями gcc можно указать, чтобы размер wchar_t при компиляции был тоже 2 байта, но тогда об использовании стандартной библиотеки от компилятора можно позабыть, т.к. она использует glibc, а та в свою очередь скомпилирована под 4-байтный wchar_t. Другими причинами были более качественная реализация стандартных классов, поддержка хеш-таблиц и даже эмуляция семантики перемещения внутри контейнеров, которой мы активно пользовались. И еще одной причиной, как говорится last but not least, была производительность строк. У нас был свой класс для строк, т.к. у нас в силу специфики нашего софта строковые операции используются очень широко и для нас это критично.

Позднее, когда Александреску работал в Facebook, с его подачи в движке Facebook была использована строка, работающая на схожих принципах (см. Наша строка основана на идеях оптимизации строк, высказанных ещё в начале 2000-х Андреем Александреску. библиотеку folly).

В нашей строке использовались две основные технологии оптимизации:

  1. Для коротких значений используется внутренний буфер в самом объекте строки (не требующий дополнительной аллокации памяти).
  2. Для всех остальных используется механика Copy On Write. Значение строки хранится в одном месте, при присвоении/модификации используется счетчик ссылок.

Чтобы ускорить компиляцию платформы, мы исключили из своего варианта STLPort реализацию stream (который мы не использовали), это дало нам ускорение компиляции примерно на 20%. Впоследствии нам пришлось ограниченно использовать Boost. Boost активно использует stream, в частности, в своих сервисных API (например, для логирования), поэтому нам приходилось модифицировать его, исключая из него использование stream. Это, в свою очередь, затрудняло нам переход на новые версии Boost.

Третий путь

При переходе на стандарт C++14 мы рассматривали такие варианты:

  1. Поднимать модифицированный нами STLPort на стандарт C++14. Опция очень непростая, т.к. поддержка STLPort была прекращена в 2010 году, и поднимать весь его код нам пришлось бы самостоятельно.
  2. Переход на другую реализацию STL, совместимую с C++14. Крайне желательно, чтобы эта реализация была под Windows и Linux.
  3. Использовать при компиляции под каждую ОС встроенную в соответствующий компилятор библиотеку.

Первый вариант был отвергнут сразу из-за слишком большого объема работ.

Чтобы портировать libc++ на Windows, пришлось бы проделать немало работы — например, писать самим всё, что связано с потоками, синхронизацией потоков и атомарностью, поскольку в libc++ в этих областях использовалось POSIX API. Мы некоторое время думали над вторым вариантом; в качестве кандидата рассматривали libc++, но он на тот момент не работал под Windows.

И мы выбрали третий путь.

Переход

Итак, нам предстояло заменить использование STLPort на библиотеки соответствующих компиляторов (Visual Studio 2015 для Windows, gcc 7 для Linux, clang 8 для macOS).

Миграция затронула 10 000 исходных файлов (из 14 000). К счастью, наш код писался в основном по гайдлайнам и не использовал всяческие хитрые трюки, так что миграция на новые библиотеки протекала сравнительно гладко, с помощью скриптов, заменяющих в исходных файлах имена типов, классов, неймспейсов и инклюдов. char16_t на всех ОС занимает 2 байта и не портит совместимость кода между Windows и Linux. wchar_t заменялся на char16_t; мы решили отказаться от использования wchar_t, т.к.

Например, в STLPort итератор можно было неявно скастить к указателю на элемент, и в некоторых местах нашего кода это использовалось. Не обошлось без небольших приключений. В новых библиотеках так делать было уже нельзя, и эти места приходилось анализировать и переписывать вручную.

Настало время тестов. Итак, миграция кода закончена, код компилируется для всех ОС.

Это было, в частности, связано с неоптимальной работой стандартных строк. Тесты после перехода показали проседание производительности (местами до 20-30%) и увеличение потребляемой памяти (до 10-15%) по сравнению со старой версией кода. Поэтому строку нам опять пришлось использовать свою, слегка доработанную.

А у нас в силу особенностей реализации в некоторых местах кода создается довольно много пустых контейнеров этого типа. Также вскрылась интересная особенность реализации контейнеров во встраиваемых библиотеках: пустые (без элементов) std::map и std::set из встроенных библиотек аллоцируют память. Поэтому мы заменили в нашем коде эти два типа контейнеров из встроенных библиотек на их реализацию от Boost, где эти контейнеры не имели такой особенности, и это решило проблему с замедлением и повышенным потреблением памяти. Аллоцируют стандартные контейнеры памяти немного, для одного корневого элемента, но для нас это оказалось критичным – на ряде сценариев у нас ощутимо упала производительность и выросло потребление памяти (по сравнению с STLPort).

Шаг за шагом мы двигались вперед, и к весне 2017 (версия 8. Как часто бывает после масштабных изменений в больших проектах, первая итерация исходников работала не без проблем, и тут нам сильно пригодились, в частности, поддержка отладочных итераторов в Windows-реализации. 11 1С:Предприятия) миграция была завершена. 3.

Итоги

Переход на стандарт С++14 занял у нас около 6 месяцев. БОльшую часть времени над проектом работал один (но очень высококвалифицированный) разработчик, а на финальной стадии подключились представители команд, ответственных за конкретные области — UI, кластер серверов, средства разработки и администрирования и т.д.

Так, версия 1С:Предприятие 8. Переход сильно упростил нам работу по миграции на новейшие версии стандарта. 14 (в разработке, релиз запланирован на начало следующего года) уже переведена на стандарт С++17. 3.

Если раньше у нас была своя доработанная версия STL и один неймспейс std, то теперь у нас в неймспейсе std находятся стандартные классы из встроенных библиотек компилятора, в неймспейсе stdx – наши, оптимизированные для наших задач строки и контейнеры, в boost – свежая версия boost. После миграции у разработчиков появилось больше возможностей. И разработчик использует те классы, которые оптимально подходят для решения его задач.

Если у класса есть конструктор перемещения и этот класс помещается в контейнер, то STL оптимизирует копирование элементов внутри контейнера (например, когда контейнер расширяется и надо изменить capacity и реаллоцировать память). Помогает в разработке также и «родная» реализация конструкторов перемещения (move constructors) для ряда классов.

Ложка дегтя

Самое, пожалуй, неприятное (но не критичное) последствие миграции — мы столкнулись с увеличением объема obj-файлов, и полный результат билда со всеми промежуточными файлами стал занимать по 60 – 70 Гб. Такое поведение связано с особенностями современных стандартных библиотек, ставших менее критично относиться к объему генерируемых служебных файлов. Это не влияет на работу скомпилированного приложения, но доставляет ряд неудобств в разработке, в частности, увеличивает время компиляции. Повышаются также требования к свободному месту на диске на билдовых серверах и на машинах разработчиков. Наши разработчики параллельно работают над несколькими версиями платформы, и сотни гигабайт промежуточных файлов иногда создают трудности в работе. Проблема неприятная, но не критичная, ее решение мы пока отложили. Как один из вариантов ее решения рассматриваем технику unity build (ее, в частности, использует Google при разработке браузера Chrome).

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть