Si përkthyem 10 milionë rreshta të kodit C++ në standardin C++14 (dhe më pas në C++17)

Disa kohë më parë (në vjeshtën e vitit 2016), gjatë zhvillimit të versionit të ardhshëm të platformës së teknologjisë 1C:Enterprise, u ngrit pyetja brenda ekipit të zhvillimit në lidhje me mbështetjen e standardit të ri C ++ 14 në kodin tonë. Kalimi në një standard të ri, siç supozuam, do të na lejonte të shkruanim shumë gjëra në mënyrë më elegante, thjesht dhe më të besueshme dhe do të thjeshtonte mbështetjen dhe mirëmbajtjen e kodit. Dhe duket se nuk ka asgjë të jashtëzakonshme në përkthim, nëse jo për shkallën e bazës së kodit dhe veçoritë specifike të kodit tonë.

Për ata që nuk e dinë, 1C:Enterprise është një mjedis për zhvillimin e shpejtë të aplikacioneve të biznesit ndër-platformë dhe kohën e ekzekutimit të tyre në OS dhe DBMS të ndryshme. Në terma të përgjithshëm, produkti përmban:

Ne përpiqemi të shkruajmë të njëjtin kod për sisteme të ndryshme operative sa më shumë që të jetë e mundur - baza e kodit të serverit është 99% e zakonshme, baza e kodit të klientit është rreth 95%. Platforma e teknologjisë 1C: Enterprise është shkruar kryesisht në C++ dhe karakteristikat e përafërta të kodit janë dhënë më poshtë:

  • 10 milionë rreshta të kodit C++,
  • 14 mijë dosje,
  • 60 mijë klasa,
  • gjysmë milioni metoda.

Dhe të gjitha këto gjëra duhej të përktheheshin në C++14. Sot do t'ju tregojmë se si e kemi bërë këtë dhe çfarë kemi hasur në proces.

Si përkthyem 10 milionë rreshta të kodit C++ në standardin C++14 (dhe më pas në C++17)

Përgjegjësia

Gjithçka e shkruar më poshtë për punën e ngadaltë/të shpejtë, (jo) konsumin e madh të memories nga zbatimet e klasave standarde në biblioteka të ndryshme do të thotë një gjë: kjo është e vërtetë PËR NE. Është mjaft e mundur që implementimet standarde të jenë më të përshtatshmet për detyrat tuaja. Ne filluam nga detyrat tona: morëm të dhëna që ishin tipike për klientët tanë, zhvilluam skenarë tipikë mbi to, shikuam performancën, sasinë e memories së konsumuar, etj., dhe analizuam nëse ne dhe klientët tanë ishim të kënaqur me rezultate të tilla apo jo . Dhe ata vepronin në varësi të.

Ajo që kishim

Fillimisht, ne shkruam kodin për platformën 1C: Enterprise 8 duke përdorur Microsoft Visual Studio. Projekti filloi në fillim të viteve 2000 dhe ne kishim një version vetëm me Windows. Natyrisht, që atëherë kodi është zhvilluar në mënyrë aktive, shumë mekanizma janë rishkruar plotësisht. Por kodi u shkrua sipas standardit të vitit 1998, dhe, për shembull, kllapat tona të këndit të drejtë u ndanë me hapësira në mënyrë që përpilimi të kishte sukses, si kjo:

vector<vector<int> > IntV;

Në vitin 2006, me lëshimin e versionit 8.1 të platformës, ne filluam të mbështesim Linux dhe kaluam në një bibliotekë standarde të palëve të treta STLPort. Një nga arsyet e tranzicionit ishte puna me linja të gjera. Në kodin tonë, ne përdorim std::wstring, i cili bazohet në llojin wchar_t, në të gjithë. Madhësia e tij në Windows është 2 bajt, dhe në Linux parazgjedhja është 4 bajt. Kjo çoi në papajtueshmëri të protokolleve tona binare midis klientit dhe serverit, si dhe të dhëna të ndryshme të vazhdueshme. Duke përdorur opsionet gcc, mund të specifikoni që madhësia e wchar_t gjatë përpilimit të jetë gjithashtu 2 bajt, por më pas mund të harroni përdorimin e bibliotekës standarde nga përpiluesi, sepse ai përdor glibc, i cili nga ana e tij përpilohet për një wchar_t 4 bajt. Arsyet e tjera ishin zbatimi më i mirë i klasave standarde, mbështetja për tabelat hash, madje edhe emulimi i semantikës së lëvizjes brenda kontejnerëve, të cilat ne i përdorëm në mënyrë aktive. Dhe një arsye më shumë, siç thonë ata së fundi, por jo më pak e rëndësishme, ishte performanca me tela. Ne kishim klasën tonë për tela, sepse... Për shkak të specifikave të softuerit tonë, operacionet e vargut përdoren shumë gjerësisht dhe për ne kjo është kritike.

Vargu ynë bazohet në idetë e optimizimit të vargut të shprehura në fillim të viteve 2000 Andrei Aleksandresku. Më vonë, kur Alexandrescu punonte në Facebook, me sugjerimin e tij u përdor një linjë në motorin e Facebook që funksiononte në parime të ngjashme (shih bibliotekën marrëzi).

Linja jonë përdori dy teknologji kryesore të optimizimit:

  1. Për vlera të shkurtra, përdoret një buffer i brendshëm në vetë objektin e vargut (që nuk kërkon ndarje shtesë të memories).
  2. Për të gjithë të tjerët, përdoret mekanika Kopjo Në Shkrim. Vlera e vargut ruhet në një vend dhe një numërues referimi përdoret gjatë caktimit/modifikimit.

Për të përshpejtuar përpilimin e platformës, ne përjashtuam zbatimin e transmetimit nga varianti ynë STLPort (të cilin nuk e përdorëm), kjo na dha rreth 20% përpilim më të shpejtë. Më pas na u desh të përdorim të kufizuar Boost. Boost e përdor shumë transmetimin, veçanërisht në API-të e tij të shërbimit (për shembull, për regjistrimin), kështu që na u desh ta modifikonim për të hequr përdorimin e transmetimit. Kjo, nga ana tjetër, e bëri të vështirë për ne migrimin në versionet e reja të Boost.

Mënyra e tretë

Kur kaluam në standardin C++14, ne morëm parasysh opsionet e mëposhtme:

  1. Përmirësoni STLPort që ne modifikuam në standardin C++14. Opsioni është shumë i vështirë, sepse... Mbështetja për STLPort u ndërpre në vitin 2010 dhe ne do të duhej ta ndërtonim vetë të gjithë kodin e tij.
  2. Kalimi në një tjetër zbatim STL të pajtueshëm me C++14. Është shumë e dëshirueshme që ky zbatim të jetë për Windows dhe Linux.
  3. Kur përpiloni për çdo OS, përdorni bibliotekën e integruar në përpiluesin përkatës.

Opsioni i parë u refuzua plotësisht për shkak të punës së tepërt.

Opsionin e dytë e menduam për ca kohë; konsiderohet si kandidat libc++, por në atë kohë nuk funksiononte nën Windows. Për të transferuar libc++ në Windows, do t'ju duhet të bëni shumë punë - për shembull, shkruani vetë gjithçka që ka të bëjë me threads, sinkronizimin e fijeve dhe atomicitetin, pasi libc++ përdoret në këto zona POSIX API.

Dhe ne zgjodhëm rrugën e tretë.

Tranzicioni

Pra, ne duhej të zëvendësonim përdorimin e STLPort me bibliotekat e përpiluesve përkatës (Visual Studio 2015 për Windows, gcc 7 për Linux, clang 8 për macOS).

Për fat të mirë, kodi ynë u shkrua kryesisht sipas udhëzimeve dhe nuk përdori të gjitha llojet e mashtrimeve të zgjuara, kështu që migrimi në bibliotekat e reja vazhdoi relativisht pa probleme, me ndihmën e skripteve që zëvendësuan emrat e llojeve, klasave, hapësirave të emrave dhe përfshin në burim. dosjet. Migrimi preku 10 skedarë burimi (nga 000). wchar_t u zëvendësua nga char14_t; vendosëm të braktisim përdorimin e wchar_t, sepse char000_t merr 16 bajt në të gjitha OS dhe nuk prish përputhshmërinë e kodit midis Windows dhe Linux.

Kishte disa aventura të vogla. Për shembull, në STLPort një përsëritës mund të hidhet në mënyrë implicite në një tregues të një elementi, dhe në disa vende në kodin tonë kjo është përdorur. Në bibliotekat e reja nuk ishte më e mundur të bëhej kjo, dhe këto pasazhe duhej të analizoheshin dhe rishkruheshin me dorë.

Pra, migrimi i kodit ka përfunduar, kodi është përpiluar për të gjitha sistemet operative. Është koha për teste.

Testet pas tranzicionit treguan një rënie të performancës (në disa vende deri në 20-30%) dhe një rritje të konsumit të kujtesës (deri në 10-15%) në krahasim me versionin e vjetër të kodit. Kjo ishte, në veçanti, për shkak të performancës jooptimale të vargjeve standarde. Prandaj, përsëri na u desh të përdornim linjën tonë, pak të modifikuar.

U zbulua gjithashtu një veçori interesante e zbatimit të kontejnerëve në bibliotekat e integruara: bosh (pa elementë) std::map dhe std::set nga bibliotekat e integruara shpërndajnë memorie. Dhe për shkak të veçorive të zbatimit, në disa vende në kod krijohen mjaft kontejnerë bosh të këtij lloji. Kontejnerët standardë të memories ndahen pak, për një element rrënjë, por për ne kjo doli të jetë kritike - në një numër skenarësh, performanca jonë ra ndjeshëm dhe konsumi i kujtesës u rrit (krahasuar me STLPort). Prandaj, në kodin tonë i zëvendësuam këto dy lloje kontejnerësh nga libraritë e integruara me implementimin e tyre nga Boost, ku këta kontejnerë nuk e kishin këtë veçori dhe kjo zgjidhi problemin me ngadalësimin dhe rritjen e konsumit të memories.

Siç ndodh shpesh pas ndryshimeve në shkallë të gjerë në projekte të mëdha, përsëritja e parë e kodit burimor nuk funksionoi pa probleme, dhe këtu, në veçanti, mbështetja për korrigjimin e përsëritësve në zbatimin e Windows erdhi në ndihmë. Hap pas hapi ne ecim përpara dhe deri në pranverën e 2017-ës (versioni 8.3.11 1C:Enterprise) migrimi përfundoi.

Rezultatet e

Kalimi në standardin C++14 na mori rreth 6 muaj. Shumicën e kohës, një zhvillues (por shumë i kualifikuar) ka punuar në projekt, dhe në fazën përfundimtare u bashkuan përfaqësues të ekipeve përgjegjëse për fusha specifike - UI, grupi i serverëve, mjetet e zhvillimit dhe administrimit, etj.

Tranzicioni e thjeshtoi shumë punën tonë për migrimin në versionet më të fundit të standardit. Kështu, versioni 1C: Enterprise 8.3.14 (në zhvillim, lëshimi i planifikuar për në fillim të vitit të ardhshëm) tashmë është transferuar në standard C++17.

Pas migrimit, zhvilluesit kanë më shumë opsione. Nëse më parë kishim versionin tonë të modifikuar të STL dhe një hapësirë ​​emri std, tani kemi klasa standarde nga bibliotekat e integruara të përpiluesit në hapësirën e emrave std, në hapësirën e emrave stdx - linjat dhe kontejnerët tanë të optimizuar për detyrat tona, në boost - versioni më i fundit i boost. Dhe zhvilluesi përdor ato klasa që janë të përshtatshme në mënyrë optimale për të zgjidhur problemet e tij.

Zbatimi "vendas" i konstruktorëve të lëvizjes gjithashtu ndihmon në zhvillim (lëvizin konstruktorët) për një numër klasash. Nëse një klasë ka një konstruktor lëvizjeje dhe kjo klasë vendoset në një kontejner, atëherë STL optimizon kopjimin e elementeve brenda kontejnerit (për shembull, kur kontejneri është zgjeruar dhe është e nevojshme të ndryshohet kapaciteti dhe të rialokohet memoria).

Fluturoni në vaj

Ndoshta pasoja më e pakëndshme (por jo kritike) e migrimit është se ne po përballemi me një rritje të volumit skedarët obj, dhe rezultati i plotë i ndërtimit me të gjithë skedarët e ndërmjetëm filloi të merrte 60–70 GB. Kjo sjellje është për shkak të veçorive të bibliotekave standarde moderne, të cilat janë bërë më pak kritike për madhësinë e skedarëve të shërbimit të krijuar. Kjo nuk ndikon në funksionimin e aplikacionit të përpiluar, por shkakton një sërë shqetësimesh në zhvillim, në veçanti, rrit kohën e përpilimit. Kërkesat për hapësirë ​​të lirë në disk në serverët e ndërtimit dhe në makinat zhvilluese po rriten gjithashtu. Zhvilluesit tanë punojnë në disa versione të platformës paralelisht, dhe qindra gigabajt skedarë të ndërmjetëm ndonjëherë krijojnë vështirësi në punën e tyre. Problemi është i pakëndshëm, por jo kritik, ne e kemi shtyrë zgjidhjen e tij tani për tani. Ne po e konsiderojmë teknologjinë si një nga opsionet për zgjidhjen e saj ndërtimin e unitetit (në veçanti, Google e përdor atë kur zhvillon shfletuesin Chrome).

Burimi: www.habr.com

Shto një koment