Як мы перавялі 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. Для ўсіх астатніх выкарыстоўваецца механіка Copy On Write. Значэнне радка захоўваецца ў адным месцы, пры прысваенні/мадыфікацыі выкарыстоўваецца лічыльнік спасылак.

Каб паскорыць кампіляцыю платформы, мы выключылі са свайго варыянту 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

Дадаць каментар