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

Cu ceva vreme în urmă (toamna anului 2016), în timpul dezvoltării unei noi versiuni a platformei tehnologice 1C:Enterprise, echipa de dezvoltare a ridicat întrebarea despre suportul pentru noul standard C++14 în codul nostru. Tranziția la noul standard, așa cum am presupus, ne-ar permite să scriem multe lucruri mai elegant, mai simplu și mai sigur, simplificând întreținerea și suportul codului. Și, aparent, nu este nimic extraordinar în traducere, dacă nu ar fi dimensiunile bazei de cod și caracteristicile specifice ale codului nostru.

Pentru cei care nu știu, 1C:Enterprise este un mediu pentru dezvoltarea rapidă de aplicații de afaceri multiplatformă și un runtime pentru execuția acestora în diferite OS-uri și SGBD-uri. În linii generale, produsul include:

Încercăm să scriem cât mai mult un singur cod pentru diferite OS-uri — baza de cod a serverului este comună în proporție de 99%, iar cea a clientului — în proporție de aproximativ 95%. Platforma tehnologică 1C:Enterprise este predominant scrisă în C++ și mai jos sunt furnizate caracteristicile aproximative ale codului:

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

Și tot acest lucru a trebuit tradus pe C++14. Despre cum am făcut asta și cu ce ne-am confruntat în proces, vom vorbi astăzi.

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

Declinarea responsabilității

Tot ce este scris mai jos despre viteza lentă/rapidă de funcționare, consumul (ne)mic de memorie al implementărilor standard în diverse biblioteci înseamnă un singur lucru: este valabil PENTRU NOI. Este posibil ca implementările standard să fie cele mai potrivite pentru sarcinile dumneavoastră. Noi ne-am bazat pe propriile noastre sarcini: am luat datele tipice pentru clienții noștri, le-am rulat prin scenarii tipice, am analizat performanța, volumul de memorie consumat etc., și am evaluat dacă rezultatele ne satisfac pe noi și pe clienții noștri sau nu. Și ne-am comportat în funcție de asta.

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 Windows. Firește, de atunci, codul s-a dezvoltat activ, multe mecanisme fiind complet rescrise. Dar codul a fost scris conform standardului din 1998, iar, de exemplu, parantezele drepte erau separate prin spații pentru a trece cu succes compilarea, astfel:

vector<vector> IntV;

În 2006, odată cu lansarea versiunii platformei 8.1, am început să suportăm Linux și am trecut la o bibliotecă standard externă STLPort. Una dintre motivele acestei tranziții a fost lucrul cu șiruri largi. În codul nostru folosim pe scară largă std::wstring, bazat pe tipul wchar_t. Dimensiunea sa în Windows este de 2 octeți, iar în Linux, implicit, de 4 octeți. Acest lucru ducea la incompatibilități între protocoalele noastre binare între client și server, precum și diverse date persistente. Opțiunile gcc pot specifica ca dimensiunea wchar_t la compilare să fie tot de 2 octeți, dar în acest caz, trebuie să renunțăm la utilizarea bibliotecii standard a compilatorului, deoarece aceasta folosește glibc, care la rândul său este compilată pentru wchar_t de 4 octeți. Alte motive au fost o implementare mai bună a claselor standard, suportul pentru tabele de dispersie și chiar emularea semanticii mutării în interiorul containerelor, pe care noi le-am utilizat activ. Și un alt motiv, cum s-ar spune, ultimul dar nu cel din urmă, a fost performanța șirurilor. Aveam propria clasă pentru șiruri, deoarece din cauza specificului software-ului nostru, operațiile pe șiruri sunt utilizate foarte pe scară largă și acest lucru este critic pentru noi.

Șirul nostru este bazat pe ideile de optimizare a șirurilor, exprimate încă la începutul anilor 2000 de Andrei Alexandrescu. Ulterior, când Alexandrescu lucra la Facebook, la propunerea lui a fost folosită în motorul Facebook o linie care funcționa pe principii asemănătoare (vezi biblioteca folly).

În linia noastră au fost folosite două tehnologii principale de optimizare:

  1. Pentru valori scurte se utilizează un buffer intern în obiectul string (care nu necesită alocare suplimentară de memorie).
  2. Pentru toate celelalte se folosește mecanica Copy On Write. Valoarea stringului este stocată într-un singur loc, la atribuirea/modificarea sa se utilizează un contor de referințe.

Pentru a accelera compilarea platformei, am exclus din varianta noastră STLPort implementarea stream (pe care nu o foloseam), acest lucru ne-a oferit un avantaj de aproximativ 20% la compilare. Ulterior, a trebuit să folosim în mod restricționat Boost. Boost folosește în mod activ stream, în special în API-urile sale de servicii (de exemplu, pentru logare), așa că a trebuit să-l modificăm, excluzând utilizarea stream-ului. Acest lucru, la rândul său, ne-a îngreunat tranziția către versiunile noi de Boost.

A treia cale

Atunci când am trecut la standardul C++14, am considerat următoarele opțiuni:

  1. Să ridicăm STLPort modificat pe standardul C++14. Opțiunea este foarte complicată, deoarece suportul pentru STLPort a fost oprit în 2010 și ar fi trebuit să ridicăm tot codul noi înșine.
  2. Trecerea la o altă implementare STL, compatibilă cu C++14. Ideal ar fi ca această implementare să fie disponibilă pentru Windows și Linux.
  3. Să folosim la compilare biblioteca încorporată în fiecare compilator corespunzător pentru fiecare sistem de operare.

Prima opțiune a fost respinsă imediat din cauza volumului prea mare de muncă.

Ne-am gândit o vreme la a doua opțiune; ca și candidat, am luat în considerare libc++, dar aceasta nu funcționa pe Windows în acel moment. Pentru a porta libc++ pe Windows, ar fi fost necesare multe muncă — de exemplu, să scriem noi tot ce ține de thread-uri, sincronizarea thread-urilor și atomicitate, deoarece în libc++ în aceste domenii era folosit POSIX API.

Și am ales a treia cale.

Trecerea

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

Din fericire, codul nostru a fost scris în principal conform liniilor directoare și nu a folosit trucuri elaborate, astfel că migrarea către biblioteci noi s-a desfășurat relativ lin, cu ajutorul scripturilor care înlocuiau în fișierele sursă numele tipurilor, claselor, spațiilor de nume și includerilor. Migrarea a afectat 10.000 de fișiere sursă (din 14.000). wchar_t a fost înlocuit cu char16_t; am decis să renunțăm la utilizarea wchar_t, deoarece char16_t ocupă 2 octeți pe toate sistemele de operare și nu compromite compatibilitatea codului între Windows și Linux.

Nu au lipsit mici aventuri. De exemplu, în STLPort iteratorul putea fi convertit implicit la un pointer către element, iar în unele locuri din codul nostru acesta a fost folosit. În noile biblioteci nu mai era posibil să se facă asta, iar aceste locuri au trebuit analizate și rescrise manual.

Așadar, migrarea codului s-a încheiat, codul se compilează pentru toate sistemele de operare. A venit timpul testelor.

Testele după migrare au arătat o scădere a performanței (în unele cazuri cu până la 20-30%) și o creștere a consumului de memorie (cu până la 10-15%) în comparație cu vechea versiune a codului. Acest lucru a fost, în parte, legat de funcționarea neoptimizată a stringurilor standard. De aceea, a trebuit să folosim din nou un string de-al nostru, ușor îmbunătățit.

De asemenea, a apărut o caracteristică interesantă a implementării containerelor în bibliotecile încorporate: std::map și std::set goale (fără elemente) din bibliotecile încorporate alocă memorie. Iar în unele locuri din codul nostru se creează un număr destul de mare de astfel de containere goale, din cauza particularităților implementării. Containerele standard alocă puțină memorie, pentru un singur element rădăcină, dar pentru noi acest lucru s-a dovedit critic – în unele scenarii am observat o scădere semnificativă a performanței și o creștere a consumului de memorie (comparativ cu STLPort). Prin urmare, am înlocuit în codul nostru aceste două tipuri de containere din bibliotecile încorporate cu implementarea lor din Boost, unde aceste containere nu aveau această caracteristică, ceea ce a rezolvat problema cu încetinirea și creșterea consumului de memorie.

Așa cum se întâmplă adesea după schimbări majore în proiecte mari, prima iterație a surselor a avut probleme, iar aici ne-a fost de mare ajutor, în special, suportul pentru iteratorii de depanare în implementarea Windows. Pas cu pas am avansat, iar până în primăvara lui 2017 (versiunea 8.3.11 1C:Enterprise) migrarea a fost finalizată.

Concluzii

Trecerea la standardul C++14 a durat aproximativ 6 luni. Majoritatea timpului, un singur dezvoltator (dar foarte bine calificat) a lucrat la proiect, iar în etapa finală s-au alăturat reprezentanții echipelor responsabile pentru domenii specifice — UI, cluster de servere, unelte de dezvoltare și administrare etc.

Trecerea a simplificat semnificativ procesul de migrare către cele mai noi versiuni ale standardului. Astfel, versiunea 1C:Enterprise 8.3.14 (în dezvoltare, lansarea este planificată pentru începutul anului viitor) a fost deja adaptată la standard. C++17.

După migrare, dezvoltatorii au avut mai multe oportunități. Dacă în trecut aveam propria noastră variantă modificată de STL și un singur spațiu de nume std, acum în spațiul de nume std se află clasele standard din bibliotecile încorporate ale compilatorului, în spațiul de nume stdx – cele optimizate pentru nevoile noastre, iar în boost – cea mai recentă versiune a boost. Dezvoltatorul folosește acele clase care se potrivesc optim pentru soluționarea problemelor sale.

De asemenea, implementarea "nativă" a constructorilor de mutare (move constructors) pentru mai multe clase ajută în dezvoltare. Dacă o clasă are un constructor de mutare și această clasă este plasată într-un container, atunci STL optimizează copierea elementelor din interiorul containerului (de exemplu, atunci când containerul se extinde și este necesară modificarea capacității și realocarea memoriei).

O lingură de cenușă

Cel mai neplăcut (dar nu critic) rezultat al migrației — ne-am confruntat cu o creștere a volumului de fișiere obj, iar rezultatul complet al compilării, cu toate fișierele intermediare, a ajuns să ocupe între 60 și 70 GB. Acest comportament este legat de caracteristicile bibliotecilor standard moderne, care au devenit mai puțin critice în ceea ce privește volumul de fișiere auxiliare generate. Acest lucru nu afectează funcționarea aplicației compilate, dar cauzează o serie de inconveniente în dezvoltare, în special mărește timpul de compilare. De asemenea, cerințele pentru spațiul liber pe disk pe serverele de compilare și pe calculatoarele dezvoltatorilor au crescut. Dezvoltatorii noștri lucrează în paralel la mai multe versiuni ale platformei, iar sute de giga de fișiere intermediare creează uneori dificultăți în muncă. Problema este neplăcută, dar nu critică; soluționarea ei a fost amânată pentru moment. Ca o alternativă la rezolvarea ei, luăm în considerare tehnica unity build (aceasta, de exemplu, este folosită de Google în dezvoltarea browser-ului Chrome).

Sursa: habr.com

Cumpără un hosting fiabil pentru site-uri cu protecție DDoS, servere VPS VDS 🔥 Cumpără un hosting fiabil pentru site-uri cu protecție DDoS, servere VPS VDS | ProHoster