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

Před časem (na podzim 2016), během vývoje další verze technologické platformy 1C:Enterprise, vyvstala ve vývojovém týmu otázka 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 mnoho věcí elegantněji, jednodušeji a spolehlivěji a zjednodušil by podporu a údržbu kódu. A zdá se, že v překladu není nic mimořádného, ​​nebýt rozsahu kódové základny a specifických rysů našeho kódu.

Pro ty, kteří nevědí, 1C:Enterprise je prostředí pro rychlý vývoj multiplatformních podnikových aplikací a runtime pro jejich spouštění na různých OS a DBMS. Obecně výrobek obsahuje:

Snažíme se co nejvíce psát stejný kód pro různé operační systémy – základna kódu serveru je z 99 % společná, základna klientského kódu je asi 95 %. Technologická platforma 1C:Enterprise je primárně napsána v C++ a přibližné charakteristiky kódu jsou uvedeny níže:

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

A všechny tyto věci musely být přeloženy do C++14. Dnes vám řekneme, jak jsme to udělali a s čím jsme se při tom setkali.

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

Zřeknutí se odpovědnosti

Vše napsané níže o pomalé/rychlé práci, (ne)velké spotřebě paměti implementacemi standardních tříd v různých knihovnách znamená jediné: to platí PRO NÁS. Je docela možné, že pro vaše úkoly budou nejvhodnější standardní implementace. Vycházeli jsme z vlastních úkolů: vzali jsme data, která byla typická pro naše klienty, spustili na nich typické scénáře, podívali se na výkon, množství spotřebované paměti atd. a analyzovali, zda jsme my a naši klienti s takovými výsledky spokojeni nebo ne. . A jednali v závislosti na.

Co jsme měli

Nejprve jsme napsali kód pro platformu 1C:Enterprise 8 v Microsoft Visual Studio. Projekt začal na počátku roku 2000 a měli jsme verzi pouze pro Windows. Přirozeně, od té doby byl kód aktivně vyvíjen, mnoho mechanismů bylo zcela přepsáno. Ale kód byl napsán podle standardu z roku 1998 a například naše pravoúhlé závorky byly odděleny mezerami, aby kompilace proběhla úspěšně, takto:

vector<vector<int> > IntV;

V roce 2006, s vydáním platformy verze 8.1, jsme začali podporovat Linux a přešli na standardní knihovnu třetích stran STLPort. Jedním z důvodů přechodu byla práce se širokými linkami. V našem kódu používáme std::wstring, který je založen na typu wchar_t. Jeho velikost ve Windows je 2 bajty a v Linuxu jsou výchozí 4 bajty. To vedlo k nekompatibilitě našich binárních protokolů mezi klientem a serverem a také k různým perzistentním datům. Pomocí voleb gcc můžete určit, že velikost wchar_t během kompilace je také 2 bajty, ale pak můžete zapomenout na použití standardní knihovny z kompilátoru, protože používá glibc, který je zase zkompilován pro 4bajtový wchar_t. Dalšími důvody byla lepší implementace standardních tříd, podpora hashovacích tabulek a dokonce emulace sémantiky pohybu uvnitř kontejnerů, kterou jsme aktivně využívali. A ještě jeden důvod, jak se říká v neposlední řadě, byl výkon strun. Měli jsme vlastní třídu pro struny, protože... Vzhledem ke specifikům našeho softwaru se operace s řetězci používají velmi široce a to je pro nás zásadní.

Náš řetězec je založen na myšlenkách optimalizace řetězců vyjádřených již na počátku 2000. století Andrej Alexandrescu. Později, když Alexandrescu pracoval ve Facebooku, na jeho návrh byla v enginu Facebooku použita linka, která fungovala na podobných principech (viz knihovna pošetilost).

Naše řada využívala dvě hlavní optimalizační technologie:

  1. Pro krátké hodnoty se používá vnitřní vyrovnávací paměť v samotném řetězcovém objektu (nevyžaduje další přidělení paměti).
  2. U všech ostatních se používá mechanika Kopírovat při zápisu. Řetězcová hodnota je uložena na jednom místě a při přiřazení/úpravě se používá referenční čítač.

Abychom urychlili kompilaci platformy, vyloučili jsme implementaci streamu z naší varianty STLPort (kterou jsme nepoužili), což nám poskytlo asi o 20 % rychlejší kompilaci. Následně jsme jej museli využít v omezené míře zesílení. Boost intenzivně využívá stream, zejména ve svých rozhraních API (například pro protokolování), takže jsme jej museli upravit, abychom odstranili použití streamu. To nám zase ztížilo migraci na nové verze Boost.

Třetí cesta

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

  1. Upgradujte STLPort, který jsme upravili, na standard C++14. Tato možnost je velmi obtížná, protože... podpora STLPort byla ukončena v roce 2010 a veškerý jeho kód bychom si museli vytvořit sami.
  2. Přechod na jinou implementaci STL kompatibilní s C++14. Je vysoce žádoucí, aby tato implementace byla pro Windows a Linux.
  3. Při kompilaci pro každý OS použijte knihovnu zabudovanou do odpovídajícího kompilátoru.

První možnost byla pro příliš mnoho práce rovnou zamítnuta.

Chvíli jsme přemýšleli o druhé možnosti; považován za kandidáta libc++, ale v té době to nefungovalo pod Windows. Chcete-li přenést libc++ na Windows, museli byste udělat spoustu práce - například sami napsat vše, co souvisí s vlákny, synchronizací vláken a atomicitou, protože libc++ se v těchto oblastech používá 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 napsán hlavně podle pokynů a nepoužíval nejrůznější chytré triky, takže migrace do nových knihoven probíhala poměrně hladce, za pomoci skriptů, které nahradily názvy typů, tříd, jmenných prostorů a zahrnuly do zdrojového kódu soubory. Migrace ovlivnila 10 000 zdrojových souborů (ze 14 000). wchar_t bylo nahrazeno char16_t; rozhodli jsme se opustit používání wchar_t, protože char16_t zabírá 2 bajty na všech operačních systémech a nenarušuje kompatibilitu kódu mezi Windows a Linuxem.

Byla tam malá dobrodružství. Například v STLPort mohl být iterátor implicitně přetypován na ukazatel na prvek a na některých místech našeho kódu to bylo použito. V nových knihovnách to již nebylo možné a tyto pasáže bylo nutné analyzovat a přepisovat ručně.

Migrace kódu je tedy dokončena, kód je zkompilován pro všechny operační systémy. Je čas na testy.

Testy po přechodu ukázaly pokles výkonu (místy až o 20-30 %) a nárůst spotřeby paměti (až o 10-15 %) oproti staré verzi kódu. Bylo to způsobeno zejména neoptimálním výkonem standardních strun. Proto jsme opět museli použít vlastní, mírně upravenou linku.

Byla také odhalena zajímavá vlastnost implementace kontejnerů ve embedded knihovnách: prázdné (bez prvků) std::map a std::set z vestavěných knihoven alokují paměť. A díky implementačním vlastnostem se na některých místech v kódu vytváří poměrně hodně prázdných kontejnerů tohoto typu. Standardní paměťové kontejnery jsou alokovány málo, pro jeden kořenový prvek, ale pro nás se to ukázalo jako kritické - v řadě scénářů se náš výkon výrazně snížil a spotřeba paměti vzrostla (ve srovnání s STLPort). Proto jsme v našem kódu nahradili tyto dva typy kontejnerů z vestavěných knihoven jejich implementací z Boost, kde tyto kontejnery tuto vlastnost neměly, a tím byl problém se zpomalením a zvýšenou spotřebou paměti vyřešen.

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

Výsledky

Přechod na standard C++14 nám trval asi 6 měsíců. Většinu času na projektu pracoval jeden (ale velmi vysoce kvalifikovaný) vývojář a v konečné fázi se zapojili zástupci týmů odpovědných za konkrétní oblasti – UI, serverový cluster, vývojové a administrační nástroje atd.

Přechod nám značně zjednodušil práci na přechodu na nejnovější verze standardu. Verze 1C:Enterprise 8.3.14 (ve vývoji, vydání naplánováno na začátek příštího roku) tedy již byla převedena do standardu C++17.

Po migraci mají vývojáři více možností. Pokud jsme dříve měli vlastní upravenou verzi STL a jeden jmenný prostor std, nyní máme standardní třídy z vestavěných knihoven kompilátorů ve jmenném prostoru std, ve jmenném prostoru stdx - naše řádky a kontejnery optimalizované pro naše úkoly, v boost - the nejnovější verze boostu. A vývojář používá ty třídy, které jsou optimálně vhodné k řešení jeho problémů.

„Nativní“ implementace konstruktorů pohybu také pomáhá ve vývoji (přesunout konstruktéry) pro řadu tříd. Pokud má třída konstruktor přesunu a tato třída je umístěna v kontejneru, pak STL optimalizuje kopírování prvků uvnitř kontejneru (například když je kontejner rozbalen a je nutné změnit kapacitu a přerozdělit paměť).

Háček

Snad nejnepříjemnějším (ale ne kritickým) důsledkem migrace je, že čelíme nárůstu objemu soubory obja úplný výsledek sestavení se všemi mezilehlými soubory začal zabírat 60–70 GB. Toto chování je způsobeno zvláštnostmi moderních standardních knihoven, které se staly méně kritickými k velikosti generovaných souborů služeb. To neovlivňuje provoz kompilované aplikace, ale způsobuje řadu nepříjemností při vývoji, zejména prodlužuje dobu kompilace. Zvyšují se také požadavky na volné místo na disku na sestavení serverů a na vývojářských strojích. Naši vývojáři pracují na několika verzích platformy paralelně a stovky gigabajtů přechodných souborů někdy způsobují potíže v jejich práci. Problém je nepříjemný, ale není kritický, jeho řešení jsme zatím odložili. Technologii zvažujeme jako jednu z možností řešení budovat jednotu (zejména jej Google používá při vývoji prohlížeče Chrome).

Zdroj: www.habr.com

Přidat komentář