Як ми переклали 10 мільйонів рядків коду C++ на стандарт C++14 (а потім і на C++17)

Якийсь час тому (восени 2016), при розробці чергової версії технологічної платформи 1С:Підприємство всередині команди розробки постало питання про підтримку нового стандарту C ++ 14 у нашому коді. Перехід на новий стандарт, як ми припускали, дозволив би нам писати багато речей елегантніше, простіше і надійніше, спрощував підтримку та супровід коду. І в перекладі начебто немає нічого екстраординарного, якби не масштаби кодової бази та специфічні особливості нашого коду.

Для тих хто не знає, 1С:Підприємство – це середовище для швидкої розробки крос-платформних бізнес-додатків та runtime для їх виконання у різних ОС та СУБД. Загалом до складу продукту входять:

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

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

І це господарство треба було перекласти на C++14. Про те, як ми це робили і з чим зіткнулися у процесі, ми сьогодні розповімо.

Як ми переклали 10 мільйонів рядків коду C++ на стандарт C++14 (а потім і на C++17)

Дисклеймер

Все написане нижче про повільну/швидку роботу, (не)велике споживання пам'яті реалізаціями стандартних класів у різних бібліотеках означає одне: це справедливо ДЛЯ НАС. Цілком можливо, для ваших завдань стандартні реалізації підійдуть якнайкраще. Ми ж відштовхувалися від своїх завдань: брали типові для наших клієнтів дані, проганяли на них типові сценарії, дивилися на швидкодію, обсяг пам'яті, що споживається тощо, і аналізували – чи влаштовують нас і наших клієнтів такі результати чи ні. І чинили залежно від.

Що в нас було

Спочатку ми писали код платформи 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 був використаний рядок, що працює на схожих принципах (див. бібліотеку дурість).

У нашому рядку використовувалися дві основні технології оптимізації:

  1. Для коротких значень використовується внутрішній буфер у самому об'єкті рядка (який не вимагає додаткової алокації пам'яті).
  2. Для решти використовується механіка Копіювати при запису. Значення рядка зберігається в одному місці, під час присвоєння/модифікації використовується лічильник посилань.

Щоб прискорити компіляцію платформи, ми виключили зі свого варіанта STLPort реалізацію stream (який ми не використовували), це дало прискорення компіляції приблизно на 20%. Згодом нам довелося обмежено використовувати Підвищення. Boost активно використовує stream, зокрема у своїх сервісних API (наприклад, для логування), тому нам доводилося модифікувати його, виключаючи з нього використання stream. Це, у свою чергу, ускладнювало перехід на нові версії Boost.

Третій шлях

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

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

Перший варіант було відкинуто одразу через занадто великий обсяг робіт.

Ми деякий час думали над другим варіантом; як кандидат розглядали 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).

Джерело: habr.com

Додати коментар або відгук