Jak jsme přeložili 10 milionů řádků kódu C++ na standard C++14 (a poté i na C++17)

Nějakou dobu zpět (na podzim 2016) došlo během vývoje další verze technologické platformy 1C:Enterprise k otázce podpory nového standardu C++14 v našem kódu. Přechod na nový standard, jak jsme předpokládali, by nám umožnil psát mnohé věci elegantněji, jednodušeji a spolehlivěji, zjednodušoval by podporu a údržbu kódu. A v překladu není nic zvláštního, pokud bychom nezohlednili rozsah kódové základny a specifické vlastnosti našeho kódu.

Pro ty, kteří neví, 1C:Enterprise je prostředí pro rychlý vývoj multiplatformních byznys aplikací a runtime pro jejich provádění na různých operačních systémech a DBMS. V obecných rysech zahrnuje produkt:

Snažíme se psát co nejvíce jeden kód pro různé OS — kódová základna serveru je společná z 99 %, klienta přibližně z 95 %. Technologická platforma 1C:Enterprise je převážně napsaná v C++ a níže uvedené jsou přibližné charakteristiky kódu:

  • 10 milionů řádků C++ kódu,
  • 14 tisíc souborů,
  • 60 tisíc tříd,
  • půl milionu metod.

A to všechno bylo potřeba převést na C++14. O tom, jak jsme to dělali a s čím jsme se setkali v procesu, vám dnes povíme.

Jak jsme přeložili 10 milionů řádků kódu C++ na standard C++14 (a poté i na C++17)

Vyloučení odpovědnosti

Vše, co je níže napsáno o pomalé/rýchlé práci, (ne)malé spotřebě paměti standardních tříd v různých knihovnách, znamená jedno: platí to PRO NÁS. Je možné, že pro vaše úkoly budou standardní implementace ty nejlepší. My jsme se však odvíjeli od našich potřeb: brali jsme typické údaje našich klientů, testovali jsme je na typických scénářích, sledovali jsme výkon, spotřebu paměti atd., a analyzovali, zda nás a naše klienty tyto výsledky uspokojují, nebo ne. A podle toho jsme se rozhodovali.

Co jsme měli

Původně jsme psali kód platformy 1C:Enterprise 8 v Microsoft Visual Studio. Projekt začal začátkem 2000. let a my jsme měli verzi pouze pro Windows. Samozřejmě, od té doby se kód aktivně rozvíjel, mnohé mechanismy byly zcela přepracovány. Ale kód byl psán podle standardu z roku 1998, a například pravé úhlové závorky byly odděleny mezerami, aby úspěšně prošla kompilace, vypadalo to takto:

vector<vector > IntV;

V roce 2006, s vydáním verze platformy 8.1, jsme začali podporovat Linux a přešli jsme na externí standardní knihovnu STLPort. Jedním z důvodů přechodu byla práce s širokými řetězci. V našem kódu běžně používáme std::wstring, založený na typu wchar_t. Jeho velikost ve Windows je 2 bajty, zatímco v Linuxu je ve výchozím nastavení 4 bajty. To vedlo k nekompatibilitě našich binárních protokolů mezi klientem a serverem, stejně jako různých trvalých dat. Možnostmi gcc lze určit, aby velikost wchar_t při kompilaci byla také 2 bajty, ale pak se můžete rozloučit s používáním standardní knihovny od překladače, protože ta používá glibc, která byla naopak zkompilována pro 4-bajtový wchar_t. Dalšími důvody byly kvalitnější implementace standardních tříd, podpora hash tabulek a dokonce i emulace přesunové semantiky uvnitř kontejnerů, kterou jsme aktivně používali. A posledním, ale neméně důležitým důvodem, byla výkonová stránka řetězců. Měli jsme svou vlastní třídu pro řetězce, protože vzhledem k specifikům našeho softwaru se řetězcové operace používají velmi široce a pro nás to bylo kritické.

Náš řetězec je založen na myšlenkách optimalizace řetězců, které byly vyřčeny již na začátku 2000. let Andrej Alexandrescu. Později, když Alexandrescu pracoval ve Facebooku, na jeho podnět byla v engine Facebooku použita řada, která pracovala na podobných principech (viz knihovna folly).

. V naší řadě byly použity dvě hlavní optimalizační technologie:

  1. Pro krátké hodnoty se používá interní vyrovnávací paměť přímo v objektu řetězce (která nevyžaduje další alokaci paměti).
  2. Pro všechny ostatní se používá mechanika Copy On Write. Hodnota řetězce je uchovávána na jednom místě, při přiřazení/modifikaci se používá počítadlo odkazů.

Abychom urychlili kompilaci platformy, vyloučili jsme ze své varianty STLPort implementaci streamu (kterou jsme nepoužívali), což nám dalo urychlení kompilace přibližně o 20 %. Později jsme museli omezeně používat Boost, Boost aktivně využívá stream, zejména ve svých API služeb (např. pro logování), takže jsme museli jeho modifikovat, abychom vyloučili jeho použití streamu. To nám na oplátku ztěžovalo přechod na nové verze Boost.

Třetí cesta

Při přechodu na standard C++14 jsme zvažovali následující varianty:

  1. Zvednout námi modifikovaný STLPort na standard C++14. Možnost je velmi složitá, protože podpora STLPort byla ukončena v roce 2010 a celý jeho kód bychom museli zvednout sami.
  2. Přejít na jinou implementaci STL kompatibilní s C++14. Je vysoce žádoucí, aby tato implementace byla pod Windows a Linux.
  3. Použít při kompilaci knihovnu zabudovanou do odpovídajícího kompilátoru pro každou OS.

První varianta byla hned zamítnuta kvůli příliš velkému objemu práce.

Nějakou dobu jsme přemýšleli o druhé variantě; jako kandidáta jsme zvažovali libc++, ale ten v té době nefungoval pod Windows. Aby bylo možné portovat libc++ na Windows, bylo by nutné udělat spoustu práce – například napsat sami vše, co se týká vláken, synchronizace vláken a atomicity, protože v libc++ se v těchto oblastech používalo POSIX API.

A zvolili jsme třetí cestu.

Přechod

Takže jsme museli nahradit použití STLPort knihovnami odpovídajících kompilátorů (Visual Studio 2015 pro Windows, gcc 7 pro Linux, clang 8 pro macOS).

Naštěstí byl náš kód psán většinou podle doporučení a nevyužíval žádné složité triky, takže migrace na nové knihovny probíhala relativně hladce, s pomocí skriptů, které v původních souborech nahrazovaly názvy typů, tříd, jmenných prostorů a zahrnutí. Migrace se dotkla 10 000 původních souborů (z 14 000). wchar_t byl nahrazen char16_t; rozhodli jsme se přestat používat wchar_t, jelikož char16_t zabírá na všech OS 2 bajty a nezhoršuje kompatibilitu kódu mezi Windows a Linuxem.

Neobešlo se to bez drobných dobrodružství. Například v STLPort bylo možné neexplicitně přetypovat iterátor na ukazatel na prvek, a v některých místech našeho kódu se to používalo. V nových knihovnách to již nebylo možné, a tato místa jsme museli analyzovat a přepisovat ručně.

Takže migrace kódu byla dokončena, kód se kompiluje pro všechny OS. Nyní přišel čas na testy.

Testy po přechodu ukázaly pokles výkonnosti ( místy až o 20-30%) a zvýšení spotřeby paměti (až o 10-15%) ve srovnání se starou verzí kódu. To bylo částečně spojeno s neoptimální prací standardních řetězců. Proto jsme opět museli použít vlastní, mírně upravenou, řetězcovou třídu.

Také se ukázala zajímavá vlastnost implementace kontejnerů ve zabudovaných knihovnách: prázdné (bez prvků) std::map a std::set z vestavěných knihoven alokují paměť. A u nás se kvůli zvláštnostem implementace v některých místech kódu vytváří docela hodně prázdných kontejnerů tohoto typu. Standardní kontejnery alokují paměť trochu, pro jeden kořenový prvek, ale pro nás to bylo kritické – v několika scénářích nám výrazně poklesla výkonnost a vzrostla spotřeba paměti (ve srovnání se STLPort). Proto jsme v našem kódu nahradili tyto dva typy kontejnerů z vestavěných knihoven jejich implementací od Boost, kde tyto kontejnery neměly takovou zvláštnost, a to vyřešilo problém se zpomalením a zvýšenou spotřebou paměti.

Jak už to bývá po rozsáhlých změnách ve velkých projektech, první iterace zdrojového kódu nefungovala bez problémů a zde nám velmi pomohla mimo jiné podpora ladicích iterátorů v implementaci pro Windows. Krok za krokem jsme se posouvali vpřed, a na jaře 2017 (verze 8.3.11 1C:Enterprise) byla migrace dokončena.

Závěry

Přechod na standard C++14 nám zabral asi 6 měsíců. Většinu času na projektu pracoval jediný (ale velmi kvalifikovaný) vývojář, na finální fázi se připojili zástupci týmů odpovědných za konkrétní oblasti — UI, serverové klastrování, vývojové a administrativní nástroje atd.

Přechod nám výrazně usnadnil práci na migraci na nejnovější verze standardu. Například verze 1C:Enterprise 8.3.14 (ve vývoji, vydání plánováno na začátek příštího roku) již byla převedena na standard. C++17.

Po migraci měli vývojáři více možností. Pokud jsme dříve používali naši upravenou verzi STL a jeden jmenný prostor std, nyní máme ve jmenném prostoru std standardní třídy z vestavěných knihoven kompilátoru, v jmenném prostoru stdx – naše optimalizované řetězce a kontejnery pro naše úkoly, a v boost – nejnovější verzi boost. A vývojář používá ty třídy, které optimálně vyhovují jeho úkolům.

Pomáhá při vývoji také „domácí“ implementace konstruktorů pohybu (move constructors) pro řadu tříd. Pokud má třída konstruktor pohybu a je umístěna v kontejneru, STL optimalizuje kopírování prvků uvnitř kontejneru (například když se kontejner rozšiřuje a je potřeba změnit kapacitu a realokovat paměť).

Lžíce dehtu

Nejspíš nejnepříjemnější (ale ne kritický) důsledek migrace — narazili jsme na zvýšení objemu obj-souborů, a celkový výsledek buildu se všemi mezifilmy začal zabírat 60 – 70 GB. Toto chování je spojeno se zvláštnostmi moderních standardních knihoven, které se staly méně kritické vůči objemu generovaných pomocných souborů. To neovlivňuje fungování zkompilované aplikace, ale přináší řadu nepříjemností při vývoji, zejména zvyšuje čas kompilace. Zvyšují se také požadavky na volné místo na discích buildových serverů a na strojích vývojářů. Naši vývojáři pracují paralelně na několika verzích platformy a stovky gigabajtů mezifilů někdy způsobují obtíže v práci. Problém je nepříjemný, ale ne kritický, jeho řešení jsme prozatím odložili. Jako jeden z možných způsobů řešení zvažujeme techniku unity build (zejména jej používá Google při vývoji prohlížeče Chrome).

Zdroj: habr.com

Koupit spolehlivý hosting pro webové stránky s ochranou proti DDoS, VPS VDS servery 🔥 Koupit spolehlivý hosting pro webové stránky s ochranou proti DDoS, VPS VDS servery | ProHoster