Ako sme preložili 10 miliónov riadkov kódu C++ do štandardu C++14 (a potom do C++17)

Pred časom (na jeseň 2016) pri vývoji ďalšej verzie technologickej platformy 1C:Enterprise vyvstala vo vývojovom tíme otázka podpory nového štandardu. C ++ 14 v našom kóde. Prechod na nový štandard, ako sme predpokladali, by nám umožnil písať mnohé veci elegantnejšie, jednoduchšie a spoľahlivejšie a zjednodušil by podporu a údržbu kódu. A zdá sa, že v preklade nie je nič mimoriadne, ak nie pre rozsah kódovej základne a špecifické vlastnosti nášho kódu.

Pre tých, ktorí nevedia, 1C:Enterprise je prostredie pre rýchly vývoj multiplatformových podnikových aplikácií a runtime na ich spúšťanie na rôznych OS a DBMS. Vo všeobecnosti produkt obsahuje:

Snažíme sa čo najviac písať rovnaký kód pre rôzne operačné systémy – základňa kódu servera je na 99 % spoločná, základňa kódu klienta je približne 95 %. Technologická platforma 1C:Enterprise je primárne napísaná v C++ a približné charakteristiky kódu sú uvedené nižšie:

  • 10 miliónov riadkov kódu C++,
  • 14 tisíc súborov,
  • 60 tisíc tried,
  • pol milióna metód.

A všetky tieto veci museli byť preložené do C++14. Dnes vám povieme, ako sme to urobili a s čím sme sa pri tom stretli.

Ako sme preložili 10 miliónov riadkov kódu C++ do štandardu C++14 (a potom do C++17)

Vylúčenie zodpovednosti

Všetko napísané nižšie o pomalej/rýchlej práci, (ne)veľkej spotrebe pamäte implementáciou štandardných tried v rôznych knižniciach znamená jedno: platí to PRE NÁS. Je celkom možné, že pre vaše úlohy budú najlepšie vyhovovať štandardné implementácie. Vychádzali sme z vlastných úloh: zobrali sme údaje, ktoré boli typické pre našich klientov, spustili sme na nich typické scenáre, pozreli sme sa na výkon, množstvo spotrebovanej pamäte atď. a analyzovali sme, či sme my a naši klienti s takýmito výsledkami spokojní alebo nie. . A konali v závislosti od.

Čo sme mali

Pôvodne sme napísali kód pre platformu 1C:Enterprise 8 v Microsoft Visual Studio. Projekt sa začal začiatkom roku 2000 a mali sme verziu iba pre Windows. Prirodzene, odvtedy sa kód aktívne vyvíjal, mnohé mechanizmy boli úplne prepísané. Ale kód bol napísaný podľa štandardu z roku 1998 a napríklad naše pravé lomené zátvorky boli oddelené medzerami, aby bola kompilácia úspešná, takto:

vector<vector<int> > IntV;

V roku 2006, s vydaním platformy verzie 8.1, sme začali podporovať Linux a prešli na štandardnú knižnicu tretích strán STLPort. Jedným z dôvodov prechodu bola práca so širokými líniami. V našom kóde používame std::wstring, ktorý je založený na type wchar_t. Jeho veľkosť v systéme Windows je 2 bajty a v systéme Linux je predvolená veľkosť 4 bajty. To viedlo k nekompatibilite našich binárnych protokolov medzi klientom a serverom, ako aj k rôznym perzistentným údajom. Pomocou volieb gcc môžete určiť, že veľkosť wchar_t počas kompilácie je tiež 2 bajty, ale potom môžete zabudnúť na používanie štandardnej knižnice z kompilátora, pretože používa glibc, ktorý je zase skompilovaný pre 4-bajtový wchar_t. Ďalšími dôvodmi bola lepšia implementácia štandardných tried, podpora hashovacích tabuliek a dokonca emulácia sémantiky pohybu vnútri kontajnerov, ktorú sme aktívne využívali. A ešte jeden dôvod, ako sa hovorí v neposlednom rade, bol výkon sláčikov. Mali sme vlastnú triedu pre struny, pretože... Vzhľadom na špecifiká nášho softvéru sa operácie s reťazcami používajú veľmi široko a pre nás je to kritické.

Náš reťazec je založený na nápadoch na optimalizáciu reťazcov vyjadrených na začiatku 2000. storočia Andrej Alexandrescu. Neskôr, keď Alexandrescu pracoval na Facebooku, na jeho návrh bola v motore Facebooku použitá linka, ktorá fungovala na podobných princípoch (pozri knižnicu pochabosť).

Naša linka využívala dve hlavné optimalizačné technológie:

  1. Pre krátke hodnoty sa používa vnútorná vyrovnávacia pamäť v samotnom reťazci (nevyžaduje dodatočné pridelenie pamäte).
  2. Na všetky ostatné sa používa mechanika Kopírovať pri zápise. Hodnota reťazca je uložená na jednom mieste a pri priraďovaní/úprave sa používa referenčné počítadlo.

Aby sme urýchlili kompiláciu platformy, vylúčili sme implementáciu streamu z nášho variantu STLPort (ktorý sme nepoužili), čo nám poskytlo asi o 20% rýchlejšiu kompiláciu. Následne sme ich museli využiť v obmedzenom rozsahu Zosilnenie. Boost intenzívne využíva stream, najmä vo svojich rozhraniach API (napríklad na protokolovanie), takže sme ho museli upraviť, aby sme odstránili používanie streamu. To nám zase sťažilo migráciu na nové verzie Boost.

tretí spôsob

Pri prechode na štandard C++14 sme zvážili nasledujúce možnosti:

  1. Aktualizujte nami upravený STLPort na štandard C++14. Táto možnosť je veľmi ťažká, pretože... podpora pre STLPort bola ukončená v roku 2010 a celý jeho kód by sme si museli vytvoriť sami.
  2. Prechod na inú implementáciu STL kompatibilnú s C++14. Je veľmi žiaduce, aby táto implementácia bola pre Windows a Linux.
  3. Pri kompilácii pre každý OS použite knižnicu zabudovanú do príslušného kompilátora.

Prvá možnosť bola pre príliš veľa práce zamietnutá.

Chvíľu sme uvažovali o druhej možnosti; považovaný za kandidáta libc++, no vtedy to pod Windowsom nefungovalo. Ak chcete preniesť libc++ na Windows, museli by ste urobiť veľa práce - napríklad sami napísať všetko, čo súvisí s vláknami, synchronizáciou vlákien a atomicitou, keďže libc++ sa v týchto oblastiach používa POSIX API.

A vybrali sme si tretiu cestu.

Prechod

Preto sme museli nahradiť použitie STLPort knižnicami zodpovedajúcich kompilátorov (Visual Studio 2015 pre Windows, gcc 7 pre Linux, clang 8 pre macOS).

Našťastie bol náš kód napísaný hlavne podľa pokynov a nepoužíval najrôznejšie šikovné triky, takže migrácia do nových knižníc prebehla pomerne hladko, pomocou skriptov, ktoré nahradili názvy typov, tried, menných priestorov a zaradili do zdrojového kódu súbory. Migrácia ovplyvnila 10 000 zdrojových súborov (zo 14 000). wchar_t bolo nahradené char16_t; rozhodli sme sa opustiť používanie wchar_t, pretože char16_t zaberá 2 bajty na všetkých operačných systémoch a nenarúša kompatibilitu kódu medzi Windows a Linux.

Bolo tam niekoľko malých dobrodružstiev. Napríklad v STLPort mohol byť iterátor implicitne pretypovaný na ukazovateľ na prvok a na niektorých miestach v našom kóde sa to použilo. V nových knižniciach to už nebolo možné a tieto pasáže sa museli analyzovať a prepisovať ručne.

Takže migrácia kódu je dokončená, kód je skompilovaný pre všetky operačné systémy. Je čas na testy.

Testy po prechode ukázali pokles výkonu (miestami až o 20 – 30 %) a zvýšenie spotreby pamäte (až o 10 – 15 %) v porovnaní so starou verziou kódu. Bolo to spôsobené najmä neoptimálnym výkonom štandardných strún. Preto sme opäť museli použiť vlastnú, mierne upravenú linku.

Odhalila sa aj zaujímavá vlastnosť implementácie kontajnerov vo vstavaných knižniciach: prázdne (bez prvkov) std::map a std::set zo vstavaných knižníc alokujú pamäť. A vďaka implementačným funkciám sa na niektorých miestach v kóde vytvára pomerne veľa prázdnych kontajnerov tohto typu. Štandardné pamäťové kontajnery sú pridelené málo, pre jeden koreňový prvok, ale pre nás sa to ukázalo ako kritické - v mnohých scenároch sa náš výkon výrazne znížil a spotreba pamäte sa zvýšila (v porovnaní s STLPort). Preto sme v našom kóde nahradili tieto dva typy kontajnerov zo vstavaných knižníc ich implementáciou z Boost, kde tieto kontajnery túto vlastnosť nemali a tým sa vyriešil problém so spomalením a zvýšenou spotrebou pamäte.

Ako to už po rozsiahlych zmenách vo veľkých projektoch býva, prvá iterácia zdrojového kódu sa nezaobišla bez problémov a tu prišla vhod najmä podpora ladiacich iterátorov v implementácii Windows. Krok za krokom sme napredovali a na jar 2017 (verzia 8.3.11 1C:Enterprise) bola migrácia dokončená.

Výsledky

Prechod na štandard C++14 nám trval približne 6 mesiacov. Na projekte väčšinou pracoval jeden (ale veľmi vysokokvalifikovaný) vývojár a v záverečnej fáze sa pridali zástupcovia tímov zodpovedných za konkrétne oblasti – UI, serverový klaster, vývojové a administračné nástroje atď.

Prechod nám výrazne zjednodušil prácu na prechode na najnovšie verzie štandardu. Verzia 1C:Enterprise 8.3.14 (vo vývoji, vydanie naplánované na začiatok budúceho roka) už bola teda prevedená na štandard C++17.

Po migrácii majú vývojári viac možností. Ak sme predtým mali vlastnú upravenú verziu STL a jeden std namespace, teraz máme štandardné triedy zo vstavaných knižníc kompilátorov v std namespace, v stdx namespace – naše riadky a kontajnery optimalizované pre naše úlohy, v boost – the najnovšia verzia boost. A vývojár používa tie triedy, ktoré sú optimálne vhodné na riešenie jeho problémov.

Pri vývoji pomáha aj „natívna“ implementácia konštruktorov pohybu (presunúť konštruktérov) pre niekoľko tried. Ak má trieda konštruktor presunu a táto trieda je umiestnená v kontajneri, potom STL optimalizuje kopírovanie prvkov vo vnútri kontajnera (napríklad keď je kontajner rozšírený a je potrebné zmeniť kapacitu a prerozdeliť pamäť).

Pozrite sa na masť

Snáď najnepríjemnejším (ale nie kritickým) dôsledkom migrácie je, že čelíme nárastu objemu obj súborya úplný výsledok zostavenia so všetkými prechodnými súbormi začal zaberať 60–70 GB. Toto správanie je spôsobené zvláštnosťami moderných štandardných knižníc, ktoré sa stali menej kritickými voči veľkosti generovaných súborov služieb. Nemá to vplyv na fungovanie skompilovanej aplikácie, ale spôsobuje to množstvo nepríjemností pri vývoji, najmä to zvyšuje čas kompilácie. Zvyšujú sa aj požiadavky na voľný diskový priestor na zostavovacích serveroch a na vývojárskych počítačoch. Naši vývojári pracujú paralelne na niekoľkých verziách platformy a stovky gigabajtov prechodných súborov niekedy spôsobujú ťažkosti pri ich práci. Problém je nepríjemný, ale nie kritický, jeho riešenie sme zatiaľ odložili. Technológiu zvažujeme ako jednu z možností riešenia budovať jednotu (Google ho používa najmä pri vývoji prehliadača Chrome).

Zdroj: hab.com

Pridať komentár