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 pentru platforma 1C:Enterprise 8 în Microsoft Visual Studio. Proiectul a început la începutul anilor 2000 și aveam o versiune numai pentru Windows. Desigur, de atunci codul a fost dezvoltat activ, multe mecanisme au fost complet rescrise. Dar codul a fost scris conform standardului din 1998 și, de exemplu, parantezele noastre în unghi drept au fost separate prin spații, astfel încât compilarea să aibă succes, astfel:

vector<vector<int> > IntV;

În 2006, odată cu lansarea versiunii 8.1 a platformei, am început să acceptăm Linux și am trecut la o bibliotecă standard terță parte STLPort. Unul dintre motivele tranziției a fost lucrul cu linii largi. În codul nostru, folosim std::wstring, care se bazează pe tipul wchar_t. Dimensiunea sa în Windows este de 2 octeți, iar în Linux este de 4 octeți în mod implicit. Acest lucru a condus la incompatibilitatea protocoalelor noastre binare între client și server, precum și la diferite date persistente. Folosind opțiunile gcc, puteți specifica că dimensiunea lui wchar_t în timpul compilării este de asemenea de 2 octeți, dar apoi puteți uita de utilizarea bibliotecii standard din compilator, deoarece folosește glibc, care la rândul său este compilat pentru un wchar_t de 4 octeți. Alte motive au fost implementarea mai bună a claselor standard, suportul pentru tabelele hash și chiar emularea semanticii deplasării în interiorul containerelor, pe care le-am folosit în mod activ. Și încă un motiv, după cum se spune, nu în ultimul rând, a fost performanța cu coarde. Aveam propria noastră clasă de coarde, pentru că... Datorită specificului software-ului nostru, operațiunile cu șiruri sunt utilizate pe scară largă și pentru noi acest lucru este esențial.

Ș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 pentru Windows și 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 acel moment nu funcționa sub Windows. Pentru a porta libc++ la Windows, ar trebui să faceți multă muncă - de exemplu, scrieți singur tot ce are legătură cu firele de execuție, sincronizarea firelor de execuție și atomicitatea, deoarece libc++ este folosit în aceste zone API POSIX.

Și am ales a treia cale.

Tranziție

Deci, 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 principal conform unor linii directoare și nu a folosit tot felul de trucuri inteligente, așa că migrarea către biblioteci noi s-a desfășurat relativ fără probleme, cu ajutorul scripturilor care au înlocuit numele de tipuri, clase, spații de nume și include în sursă. fişiere. Migrarea a afectat 10 de fișiere sursă (din 000). wchar_t a fost înlocuit cu char14_t; am decis să renunțăm la utilizarea wchar_t, deoarece char000_t are 16 octeți pe toate sistemele de operare și nu strica compatibilitatea codului dintre Windows și 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ă modificări la scară largă în proiecte mari, prima iterație a codului sursă nu a funcționat fără probleme și aici, în special, suportul pentru depanarea iteratoarelor în implementarea Windows a fost util. Pas cu pas am mers înainte, iar până în primăvara lui 2017 (versiunea 8.3.11 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

Adauga un comentariu