Некоторое время назад (осенью 2016), при разработке очередной версии технологической платформы 1С:Предприятие внутри команды разработки встал вопрос о поддержке нового стандарта C++14 в нашем коде. Переход на новый стандарт, как мы предполагали, позволил бы нам писать многие вещи элегантней, проще и надежней, упрощал поддержку и сопровождение кода. И в переводе вроде бы нет ничего экстраординарного, если бы не масштабы кодовой базы и специфические особенности нашего кода.
Для тех кто не знает, 1С:Предприятие – это среда для быстрой разработки кросс-платформенных бизнес-приложений и runtime для их выполнения в разных ОС и СУБД. В общих чертах в состав продукта входят:
Мы стараемся по максимуму писать один код для разных ОС — кодовая база сервера общая на 99%, клиента — примерно на 95%. Технологическая платформа 1С:Предприятие преимущественно написана на C++ и ниже приведены приблизительные характеристики кода:
И все это хозяйство надо было перевести на C++14. О том, как мы это делали и с чем столкнулись в процессе, мы сегодня и расскажем.
Все написанное ниже о медленной/быстрой работе, (не)большом потреблении памяти реализациями стандартных классов в различных библиотеках означает одно: это справедливо ДЛЯ НАС. Вполне возможно, для ваших задач стандартные реализации подойдут наилучшим образом. Мы же отталкивались от своих задач: брали типичные для наших клиентов данные, прогоняли на них типичные сценарии, смотрели на быстродействие, объем потребляемой памяти и т.п., и анализировали – устраивают ли нас и наших клиентов такие результаты или нет. И поступали в зависимости от.
Изначально мы писали код платформы 1С:Предприятие 8 на Microsoft Visual Studio. Проект начался в начале 2000-х и у нас была версия только под Windows. Естественно, с тех пор код активно развивался, многие механизмы были полностью переписаны. Но код писался по стандарту 1998 года, и, например, правые угловые скобки у нас были разделены пробелами, чтобы успешно проходила компиляция, вот так:
В 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, была производительность строк. У нас был свой класс для строк, т.к. у нас в силу специфики нашего софта строковые операции используются очень широко и для нас это критично.
Наша строка основана на идеях оптимизации строк, высказанных ещё в начале 2000-х Андреем Александреску. Позднее, когда Александреску работал в Facebook, с его подачи в движке Facebook была использована строка, работающая на схожих принципах (см. библиотеку folly).
В нашей строке использовались две основные технологии оптимизации:
Чтобы ускорить компиляцию платформы, мы исключили из своего варианта STLPort реализацию stream (который мы не использовали), это дало нам ускорение компиляции примерно на 20%. Впоследствии нам пришлось ограниченно использовать Boost. Boost активно использует stream, в частности, в своих сервисных API (например, для логирования), поэтому нам приходилось модифицировать его, исключая из него использование stream. Это, в свою очередь, затрудняло нам переход на новые версии Boost.
При переходе на стандарт C++14 мы рассматривали такие варианты:
Первый вариант был отвергнут сразу из-за слишком большого объема работ.
Мы некоторое время думали над вторым вариантом; в качестве кандидата рассматривали libc++, но он на тот момент не работал под Windows. Чтобы портировать libc++ на Windows, пришлось бы проделать немало работы — например, писать самим всё, что связано с потоками, синхронизацией потоков и атомарностью, поскольку в libc++ в этих областях использовалось POSIX API.
И мы выбрали третий путь.
Итак, нам предстояло заменить использование STLPort на библиотеки соответствующих компиляторов (Visual Studio 2015 для Windows, gcc 7 для Linux, clang 8 для macOS).
К счастью, наш код писался в основном по гайдлайнам и не использовал всяческие хитрые трюки, так что миграция на новые библиотеки протекала сравнительно гладко, с помощью скриптов, заменяющих в исходных файлах имена типов, классов, неймспейсов и инклюдов. Миграция затронула 10 000 исходных файлов (из 14 000). wchar_t заменялся на char16_t; мы решили отказаться от использования wchar_t, т.к. char16_t на всех ОС занимает 2 байта и не портит совместимость кода между Windows и Linux.
Не обошлось без небольших приключений. Например, в STLPort итератор можно было неявно скастить к указателю на элемент, и в некоторых местах нашего кода это использовалось. В новых библиотеках так делать было уже нельзя, и эти места приходилось анализировать и переписывать вручную.
Итак, миграция кода закончена, код компилируется для всех ОС. Настало время тестов.
Тесты после перехода показали проседание производительности (местами до 20-30%) и увеличение потребляемой памяти (до 10-15%) по сравнению со старой версией кода. Это было, в частности, связано с неоптимальной работой стандартных строк. Поэтому строку нам опять пришлось использовать свою, слегка доработанную.
Также вскрылась интересная особенность реализации контейнеров во встраиваемых библиотеках: пустые (без элементов) std::map и std::set из встроенных библиотек аллоцируют память. А у нас в силу особенностей реализации в некоторых местах кода создается довольно много пустых контейнеров этого типа. Аллоцируют стандартные контейнеры памяти немного, для одного корневого элемента, но для нас это оказалось критичным – на ряде сценариев у нас ощутимо упала производительность и выросло потребление памяти (по сравнению с STLPort). Поэтому мы заменили в нашем коде эти два типа контейнеров из встроенных библиотек на их реализацию от Boost, где эти контейнеры не имели такой особенности, и это решило проблему с замедлением и повышенным потреблением памяти.
Как часто бывает после масштабных изменений в больших проектах, первая итерация исходников работала не без проблем, и тут нам сильно пригодились, в частности, поддержка отладочных итераторов в Windows-реализации. Шаг за шагом мы двигались вперед, и к весне 2017 (версия 8.3.11 1С:Предприятия) миграция была завершена.
Переход на стандарт С++14 занял у нас около 6 месяцев. БОльшую часть времени над проектом работал один (но очень высококвалифицированный) разработчик, а на финальной стадии подключились представители команд, ответственных за конкретные области — UI, кластер серверов, средства разработки и администрирования и т.д.
Переход сильно упростил нам работу по миграции на новейшие версии стандарта. Так, версия 1С:Предприятие 8.3.14 (в разработке, релиз запланирован на начало следующего года) уже переведена на стандарт С++17.
После миграции у разработчиков появилось больше возможностей. Если раньше у нас была своя доработанная версия STL и один неймспейс std, то теперь у нас в неймспейсе std находятся стандартные классы из встроенных библиотек компилятора, в неймспейсе stdx – наши, оптимизированные для наших задач строки и контейнеры, в boost – свежая версия boost. И разработчик использует те классы, которые оптимально подходят для решения его задач.
Помогает в разработке также и «родная» реализация конструкторов перемещения (move constructors) для ряда классов. Если у класса есть конструктор перемещения и этот класс помещается в контейнер, то STL оптимизирует копирование элементов внутри контейнера (например, когда контейнер расширяется и надо изменить capacity и реаллоцировать память).
Самое, пожалуй, неприятное (но не критичное) последствие миграции — мы столкнулись с увеличением объема obj-файлов, и полный результат билда со всеми промежуточными файлами стал занимать по 60 – 70 Гб. Такое поведение связано с особенностями современных стандартных библиотек, ставших менее критично относиться к объему генерируемых служебных файлов. Это не влияет на работу скомпилированного приложения, но доставляет ряд неудобств в разработке, в частности, увеличивает время компиляции. Повышаются также требования к свободному месту на диске на билдовых серверах и на машинах разработчиков. Наши разработчики параллельно работают над несколькими версиями платформы, и сотни гигабайт промежуточных файлов иногда создают трудности в работе. Проблема неприятная, но не критичная, ее решение мы пока отложили. Как один из вариантов ее решения рассматриваем технику unity build (ее, в частности, использует Google при разработке браузера Chrome).
Для тех кто не знает, 1С:Предприятие – это среда для быстрой разработки кросс-платформенных бизнес-приложений и runtime для их выполнения в разных ОС и СУБД. В общих чертах в состав продукта входят:
- Кластер серверов приложений, работает на Windows и Linux
- Клиент, работающий с сервером по http(s) или по собственному бинарному протоколу, работает на Windows, Linux, macOS
- Веб-клиент, работающий в браузерах Chrome, Internet Explorer, Microsoft Edge, Firefox, Safari (написан на JavaScript)
- Среда разработки (Конфигуратор), работает на 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. О том, как мы это делали и с чем столкнулись в процессе, мы сегодня и расскажем.
Дисклеймер
Все написанное ниже о медленной/быстрой работе, (не)большом потреблении памяти реализациями стандартных классов в различных библиотеках означает одно: это справедливо ДЛЯ НАС. Вполне возможно, для ваших задач стандартные реализации подойдут наилучшим образом. Мы же отталкивались от своих задач: брали типичные для наших клиентов данные, прогоняли на них типичные сценарии, смотрели на быстродействие, объем потребляемой памяти и т.п., и анализировали – устраивают ли нас и наших клиентов такие результаты или нет. И поступали в зависимости от.
Что у нас было
Изначально мы писали код платформы 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, была производительность строк. У нас был свой класс для строк, т.к. у нас в силу специфики нашего софта строковые операции используются очень широко и для нас это критично.
Наша строка основана на идеях оптимизации строк, высказанных ещё в начале 2000-х Андреем Александреску. Позднее, когда Александреску работал в Facebook, с его подачи в движке Facebook была использована строка, работающая на схожих принципах (см. библиотеку folly).
В нашей строке использовались две основные технологии оптимизации:
- Для коротких значений используется внутренний буфер в самом объекте строки (не требующий дополнительной аллокации памяти).
- Для всех остальных используется механика Copy On Write. Значение строки хранится в одном месте, при присвоении/модификации используется счетчик ссылок.
Чтобы ускорить компиляцию платформы, мы исключили из своего варианта STLPort реализацию stream (который мы не использовали), это дало нам ускорение компиляции примерно на 20%. Впоследствии нам пришлось ограниченно использовать Boost. Boost активно использует stream, в частности, в своих сервисных API (например, для логирования), поэтому нам приходилось модифицировать его, исключая из него использование stream. Это, в свою очередь, затрудняло нам переход на новые версии Boost.
Третий путь
При переходе на стандарт C++14 мы рассматривали такие варианты:
- Поднимать модифицированный нами STLPort на стандарт C++14. Опция очень непростая, т.к. поддержка STLPort была прекращена в 2010 году, и поднимать весь его код нам пришлось бы самостоятельно.
- Переход на другую реализацию STL, совместимую с C++14. Крайне желательно, чтобы эта реализация была под Windows и Linux.
- Использовать при компиляции под каждую ОС встроенную в соответствующий компилятор библиотеку.
Первый вариант был отвергнут сразу из-за слишком большого объема работ.
Мы некоторое время думали над вторым вариантом; в качестве кандидата рассматривали libc++, но он на тот момент не работал под Windows. Чтобы портировать libc++ на Windows, пришлось бы проделать немало работы — например, писать самим всё, что связано с потоками, синхронизацией потоков и атомарностью, поскольку в libc++ в этих областях использовалось POSIX API.
И мы выбрали третий путь.
Переход
Итак, нам предстояло заменить использование STLPort на библиотеки соответствующих компиляторов (Visual Studio 2015 для Windows, gcc 7 для Linux, clang 8 для macOS).
К счастью, наш код писался в основном по гайдлайнам и не использовал всяческие хитрые трюки, так что миграция на новые библиотеки протекала сравнительно гладко, с помощью скриптов, заменяющих в исходных файлах имена типов, классов, неймспейсов и инклюдов. Миграция затронула 10 000 исходных файлов (из 14 000). wchar_t заменялся на char16_t; мы решили отказаться от использования wchar_t, т.к. char16_t на всех ОС занимает 2 байта и не портит совместимость кода между Windows и Linux.
Не обошлось без небольших приключений. Например, в STLPort итератор можно было неявно скастить к указателю на элемент, и в некоторых местах нашего кода это использовалось. В новых библиотеках так делать было уже нельзя, и эти места приходилось анализировать и переписывать вручную.
Итак, миграция кода закончена, код компилируется для всех ОС. Настало время тестов.
Тесты после перехода показали проседание производительности (местами до 20-30%) и увеличение потребляемой памяти (до 10-15%) по сравнению со старой версией кода. Это было, в частности, связано с неоптимальной работой стандартных строк. Поэтому строку нам опять пришлось использовать свою, слегка доработанную.
Также вскрылась интересная особенность реализации контейнеров во встраиваемых библиотеках: пустые (без элементов) std::map и std::set из встроенных библиотек аллоцируют память. А у нас в силу особенностей реализации в некоторых местах кода создается довольно много пустых контейнеров этого типа. Аллоцируют стандартные контейнеры памяти немного, для одного корневого элемента, но для нас это оказалось критичным – на ряде сценариев у нас ощутимо упала производительность и выросло потребление памяти (по сравнению с STLPort). Поэтому мы заменили в нашем коде эти два типа контейнеров из встроенных библиотек на их реализацию от Boost, где эти контейнеры не имели такой особенности, и это решило проблему с замедлением и повышенным потреблением памяти.
Как часто бывает после масштабных изменений в больших проектах, первая итерация исходников работала не без проблем, и тут нам сильно пригодились, в частности, поддержка отладочных итераторов в Windows-реализации. Шаг за шагом мы двигались вперед, и к весне 2017 (версия 8.3.11 1С:Предприятия) миграция была завершена.
Итоги
Переход на стандарт С++14 занял у нас около 6 месяцев. БОльшую часть времени над проектом работал один (но очень высококвалифицированный) разработчик, а на финальной стадии подключились представители команд, ответственных за конкретные области — UI, кластер серверов, средства разработки и администрирования и т.д.
Переход сильно упростил нам работу по миграции на новейшие версии стандарта. Так, версия 1С:Предприятие 8.3.14 (в разработке, релиз запланирован на начало следующего года) уже переведена на стандарт С++17.
После миграции у разработчиков появилось больше возможностей. Если раньше у нас была своя доработанная версия STL и один неймспейс std, то теперь у нас в неймспейсе std находятся стандартные классы из встроенных библиотек компилятора, в неймспейсе stdx – наши, оптимизированные для наших задач строки и контейнеры, в boost – свежая версия boost. И разработчик использует те классы, которые оптимально подходят для решения его задач.
Помогает в разработке также и «родная» реализация конструкторов перемещения (move constructors) для ряда классов. Если у класса есть конструктор перемещения и этот класс помещается в контейнер, то STL оптимизирует копирование элементов внутри контейнера (например, когда контейнер расширяется и надо изменить capacity и реаллоцировать память).
Ложка дегтя
Самое, пожалуй, неприятное (но не критичное) последствие миграции — мы столкнулись с увеличением объема obj-файлов, и полный результат билда со всеми промежуточными файлами стал занимать по 60 – 70 Гб. Такое поведение связано с особенностями современных стандартных библиотек, ставших менее критично относиться к объему генерируемых служебных файлов. Это не влияет на работу скомпилированного приложения, но доставляет ряд неудобств в разработке, в частности, увеличивает время компиляции. Повышаются также требования к свободному месту на диске на билдовых серверах и на машинах разработчиков. Наши разработчики параллельно работают над несколькими версиями платформы, и сотни гигабайт промежуточных файлов иногда создают трудности в работе. Проблема неприятная, но не критичная, ее решение мы пока отложили. Как один из вариантов ее решения рассматриваем технику unity build (ее, в частности, использует Google при разработке браузера Chrome).