Cum am tradus 10 milioane de linii de cod C++ la standardul C++14 (și apoi la C++17)

Cu ceva timp în urmă (în toamna lui 2016), în timpul dezvoltării următoarei versiuni a platformei tehnologice 1C:Enterprise, a apărut întrebarea în cadrul echipei de dezvoltare cu privire la suportarea noului standard C ++ 14 în codul nostru. Trecerea la un nou standard, așa cum am presupus, ne-ar permite să scriem multe lucruri mai elegant, mai simplu și mai fiabil și ar simplifica suportul și întreținerea codului. Și nu pare să existe nimic extraordinar în traducere, dacă nu pentru amploarea bazei de cod și caracteristicile specifice ale codului nostru.

Pentru cei care nu știu, 1C:Enterprise este un mediu pentru dezvoltarea rapidă a aplicațiilor de afaceri multiplatforme și a timpului de execuție pentru executarea acestora pe diferite sisteme de operare și SGBD. În general, produsul conține:

Încercăm să scriem același cod pentru diferite sisteme de operare cât mai mult posibil - baza de cod de server este comună în proporție de 99%, baza de cod de client este de aproximativ 95%. Platforma tehnologică 1C:Enterprise este scrisă în principal în C++, iar caracteristicile aproximative ale codului sunt prezentate mai jos:

  • 10 milioane de linii de cod C++,
  • 14 mii de fișiere,
  • 60 de mii de clase,
  • jumătate de milion de metode.

Și toate aceste lucruri trebuiau traduse în C++14. Astăzi vă vom spune cum am făcut acest lucru și ce am întâlnit în acest proces.

Cum am tradus 10 milioane de linii de cod C++ la standardul C++14 (și apoi la C++17)

Disclaimer

Tot ceea ce este scris mai jos despre lucrul lent/rapid, consumul (nu) mare de memorie de către implementările de clase standard în diferite biblioteci înseamnă un lucru: acest lucru este adevărat PENTRU NOI. Este foarte posibil ca implementările standard să fie cele mai potrivite pentru sarcinile dvs. Am pornit de la propriile sarcini: am luat date care erau tipice pentru clienții noștri, am rulat scenarii tipice pe ele, am analizat performanța, cantitatea de memorie consumată etc. și am analizat dacă noi și clienții noștri suntem mulțumiți de astfel de rezultate sau nu . Și au acționat în funcție de.

Ce am avut

Inițial, am scris codul platformei 1C:Enterprise 8 în Microsoft Visual Studio. Proiectul a început la începutul anilor 2000 și aveam doar o versiune pentru WindowsFirește, codul a evoluat rapid de atunci, iar multe mecanisme au fost complet rescrise. Dar codul a fost scris conform standardului din 1998 și, de exemplu, am separat parantezele unghiulare drepte cu spații pentru a asigura o compilare reușită, astfel:

vector<vector<int> > IntV;

În 2006, odată cu lansarea versiunii 8.1 a platformei, am început să oferim suport Linux și a trecut la o bibliotecă standard terță parte STLPortUnul dintre motivele pentru schimbare a fost lucrul cu șiruri de caractere largi. În codul nostru, folosim std::wstring, care se bazează pe tipul wchar_t. Dimensiunea sa este Windows 2 octeți și în Linux Valoarea implicită este de 4 octeți. Aceasta a dus la incompatibilitate între protocoalele noastre binare dintre client și server, precum și la diverse date persistente. Folosind opțiunile gcc, puteți forța dimensiunea wchar_t să fie de 2 octeți în timpul compilării, dar apoi puteți uita de utilizarea bibliotecii standard a compilatorului, deoarece aceasta folosește glibc, care este la rândul ei compilată pentru un wchar_t de 4 octeți. Alte motive au inclus o implementare de calitate superioară a claselor standard, suport pentru tabele hash și chiar emularea semanticii de mișcare a containerelor, pe care am folosit-o în mod activ. Și nu în ultimul rând, așa cum se spune, a fost performanța șirurilor de caractere. Am avut propria noastră clasă de șiruri de caractere, deoarece operațiile cu șiruri de caractere sunt utilizate pe scară largă în software-ul nostru, iar acest lucru este esențial pentru noi.

Șirul nostru se bazează pe idei de optimizare a șirurilor exprimate la începutul anilor 2000 Andrei Alexandrescu. Mai târziu, când Alexandrescu a lucrat la Facebook, la propunerea lui, a fost folosită o linie în motorul Facebook care a funcționat pe principii similare (vezi biblioteca nebunie).

Linia noastră a folosit două tehnologii principale de optimizare:

  1. Pentru valori scurte, se folosește un buffer intern în obiectul șir în sine (nu necesită alocare suplimentară de memorie).
  2. Pentru toate celelalte se folosește mecanica Copie pe scriere. Valoarea șirului este stocată într-un singur loc și un contor de referință este utilizat în timpul atribuirii/modificării.

Pentru a accelera compilarea platformei, am exclus implementarea fluxului din varianta noastră STLPort (pe care nu am folosit-o), acest lucru ne-a oferit o compilare cu aproximativ 20% mai rapidă. Ulterior a trebuit să facem o utilizare limitată Sprijini. Boost folosește intens fluxul, în special în API-urile sale de serviciu (de exemplu, pentru înregistrare), așa că a trebuit să îl modificăm pentru a elimina utilizarea fluxului. Acest lucru, la rândul său, ne-a îngreunat migrarea la versiuni noi de Boost.

a treia cale

Când am trecut la standardul C++14, am luat în considerare următoarele opțiuni:

  1. Actualizați STLPort pe care l-am modificat la standardul C++14. Opțiunea este foarte dificilă, pentru că... suportul pentru STLPort a fost întrerupt în 2010 și ar trebui să construim singuri tot codul acestuia.
  2. Trecerea la o altă implementare STL compatibilă cu C++14. Este foarte de dorit ca această implementare să fie compatibilă cu Windows и Linux.
  3. Când compilați pentru fiecare sistem de operare, utilizați biblioteca încorporată în compilatorul corespunzător.

Prima opțiune a fost respinsă definitiv din cauza prea multă muncă.

Ne-am gândit de ceva vreme la a doua variantă; considerat candidat libc++, dar la acea vreme nu lucra sub WindowsPentru a porta libc++ la Windows, ar trebui să depunem multă muncă - de exemplu, să scriem singuri tot ce ține de thread-uri, sincronizarea thread-urilor și atomicitate, deoarece libc++ folosea API POSIX.

Și am ales a treia cale.

Tranziție

Așadar, a trebuit să înlocuim utilizarea STLPort cu bibliotecile compilatoarelor corespunzătoare (Visual Studio 2015 pentru Windows, gcc 7 pentru Linux, clang 8 pentru macOS).

Din fericire, codul nostru a fost scris în mare parte conform instrucțiunilor și nu a folosit niciun truc ingenios, așa că migrarea către noile biblioteci a decurs relativ fără probleme, folosind scripturi care au înlocuit numele tipurilor, claselor, spațiilor de nume și includerilor din fișierele sursă. Migrarea a afectat 10.000 de fișiere sursă (din 14.000). wchar_t a fost înlocuit cu char16_t; am decis să abandonăm utilizarea lui wchar_t, deoarece char16_t ocupă 2 octeți pe toate sistemele de operare și nu afectează compatibilitatea codului dintre... Windows и Linux.

Au fost niște mici aventuri. De exemplu, în STLPort un iterator ar putea fi implicit turnat la un pointer către un element, iar în unele locuri în codul nostru a fost folosit. În bibliotecile noi nu se mai putea face acest lucru, iar aceste pasaje trebuiau analizate și rescrise manual.

Deci, migrarea codului este completă, codul este compilat pentru toate sistemele de operare. E timpul pentru teste.

Testele de după tranziție au arătat o scădere a performanței (în unele locuri până la 20-30%) și o creștere a consumului de memorie (până la 10-15%) față de versiunea veche a codului. Acest lucru a fost, în special, din cauza performanței suboptime a șirurilor standard. Prin urmare, a trebuit din nou să folosim propria noastră linie, ușor modificată.

A fost dezvăluită și o caracteristică interesantă a implementării containerelor în bibliotecile încorporate: gol (fără elemente) std::map și std::set din bibliotecile încorporate alocă memorie. Și datorită caracteristicilor de implementare, în unele locuri în cod sunt create destul de multe containere goale de acest tip. Containerele de memorie standard sunt alocate puțin, pentru un element rădăcină, dar pentru noi acest lucru sa dovedit a fi critic - într-o serie de scenarii, performanța noastră a scăzut semnificativ și consumul de memorie a crescut (comparativ cu STLPort). Prin urmare, în codul nostru am înlocuit aceste două tipuri de containere din bibliotecile încorporate cu implementarea lor din Boost, unde aceste containere nu aveau această caracteristică, iar acest lucru a rezolvat problema cu încetinirea și consumul crescut de memorie.

Așa cum se întâmplă adesea după schimbări la scară largă în proiecte mari, prima iterație a codului sursă nu a fost lipsită de probleme, și aici, în special, suportul pentru iteratorii de depanare în Windows-implementare. Am avansat pas cu pas, iar până în primăvara anului 2017 (versiunea 8.3.11 a 1C:Enterprise), migrarea a fost finalizată.

Rezultatele

Trecerea la standardul C++14 ne-a luat aproximativ 6 luni. De cele mai multe ori, la proiect a lucrat un dezvoltator (dar foarte înalt calificat), iar în etapa finală s-au alăturat reprezentanți ai echipelor responsabile pentru domenii specifice - UI, cluster de servere, instrumente de dezvoltare și administrare etc.

Tranziția a simplificat foarte mult munca noastră privind migrarea la cele mai recente versiuni ale standardului. Astfel, versiunea 1C:Enterprise 8.3.14 (în dezvoltare, lansare programată pentru începutul anului viitor) a fost deja transferată la standard C++17.

După migrare, dezvoltatorii au mai multe opțiuni. Dacă mai devreme aveam propria noastră versiune modificată de STL și un spațiu de nume std, acum avem clase standard din bibliotecile compilatoare încorporate în spațiul de nume std, în spațiul de nume stdx - liniile și containerele noastre optimizate pentru sarcinile noastre, în boost - cea mai recentă versiune de boost. Și dezvoltatorul folosește acele clase care sunt potrivite optim pentru a-și rezolva problemele.

Implementarea „nativă” a constructorilor de mutare ajută și la dezvoltare (muta constructorii) pentru un număr de clase. Dacă o clasă are un constructor de mutare și această clasă este plasată într-un container, atunci STL-ul optimizează copierea elementelor din interiorul containerului (de exemplu, atunci când containerul este extins și este necesară modificarea capacității și realocarea memoriei).

Fly în Unguent

Poate cea mai neplăcută (dar nu critică) consecință a migrației este că ne confruntăm cu o creștere a volumului fișierele obj, iar rezultatul complet al construcției cu toate fișierele intermediare a început să ocupe 60–70 GB. Acest comportament se datorează particularităților bibliotecilor standard moderne, care au devenit mai puțin critice față de dimensiunea fișierelor de serviciu generate. Acest lucru nu afectează funcționarea aplicației compilate, dar provoacă o serie de inconveniente în dezvoltare, în special, crește timpul de compilare. Cerințele pentru spațiul liber pe disc pe serverele de compilare și pe mașinile de dezvoltator sunt, de asemenea, în creștere. Dezvoltatorii noștri lucrează pe mai multe versiuni ale platformei în paralel, iar sute de gigaocteți de fișiere intermediare creează uneori dificultăți în munca lor. Problema este neplăcută, dar nu critică; am amânat soluția ei pentru moment. Considerăm tehnologia ca una dintre opțiunile de rezolvare construirea unității (în special, Google îl folosește atunci când dezvoltă browserul Chrome).

Sursa: www.habr.com

Cumpărați găzduire de încredere pentru site-uri cu protecție DDoS, servere VPS VDS 🔥 Cumpără găzduire web fiabilă cu protecție DDoS, servere VPS VDS | ProHoster