Kako smo prevedli 10 milijonov vrstic kode C++ v standard C++14 (in nato v C++17)

Pred časom (jeseni 2016) se je med razvojem naslednje različice tehnološke platforme 1C:Enterprise znotraj razvojne ekipe pojavilo vprašanje o podpori novega standarda. C ++ 14 v naši kodi. Prehod na nov standard bi nam, kot smo predvidevali, omogočil bolj elegantno, preprosto in zanesljivo pisanje marsičesa ter poenostavil podporo in vzdrževanje kode. In zdi se, da v prevodu ni nič nenavadnega, če ne za obseg kodne baze in posebne značilnosti naše kode.

Za tiste, ki ne vedo, je 1C:Enterprise okolje za hiter razvoj večplatformskih poslovnih aplikacij in izvajalnega okolja za njihovo izvajanje na različnih OS in DBMS. Na splošno izdelek vsebuje:

Trudimo se čim bolj pisati enako kodo za različne operacijske sisteme – kodna baza strežnika je 99 % skupna, kodna baza odjemalca je približno 95 %. Tehnološka platforma 1C:Enterprise je primarno napisana v C++ in približne značilnosti kode so navedene spodaj:

  • 10 milijonov vrstic kode C++,
  • 14 tisoč datotek,
  • 60 tisoč razredov,
  • pol milijona metod.

In vse te stvari je bilo treba prevesti v C++14. Danes vam bomo povedali, kako smo to storili in na kaj smo pri tem naleteli.

Kako smo prevedli 10 milijonov vrstic kode C++ v standard C++14 (in nato v C++17)

Izjava o omejitvi odgovornosti

Vse spodaj napisano o počasnem/hitrem delu, (ne)veliki porabi pomnilnika pri implementacijah standardnih razredov v različnih knjižnicah pomeni eno: to velja ZA NAS. Povsem mogoče je, da bodo standardne izvedbe najbolj primerne za vaše naloge. Izhajali smo iz lastnih nalog: vzeli smo podatke, ki so značilni za naše stranke, na njih izvajali tipične scenarije, gledali na zmogljivost, količino porabljenega pomnilnika itd. ter analizirali, ali smo mi in naše stranke s takšnimi rezultati zadovoljni ali ne. . In ravnali so glede na.

Kar smo imeli

Na začetku smo pisali kodo za platformo 1C:Enterprise 8 s programom Microsoft Visual Studio. Projekt se je začel v začetku leta 2000 in imeli smo različico samo za Windows. Seveda se je od takrat koda aktivno razvijala, številni mehanizmi so bili popolnoma prepisani. Toda koda je bila napisana v skladu s standardom iz leta 1998 in na primer naši desni kotni oklepaji so bili ločeni s presledki, da bi prevajanje uspelo, takole:

vector<vector<int> > IntV;

Leta 2006 smo z izdajo različice platforme 8.1 začeli podpirati Linux in prešli na standardno knjižnico drugega proizvajalca STLPort. Eden od razlogov za prehod je bilo delo s širokimi linijami. V naši kodi vseskozi uporabljamo std::wstring, ki temelji na vrsti wchar_t. Njegova velikost v sistemu Windows je 2 bajta, v sistemu Linux pa je privzeta 4 bajta. To je privedlo do nezdružljivosti naših binarnih protokolov med odjemalcem in strežnikom, pa tudi do različnih obstojnih podatkov. Z možnostmi gcc lahko določite, da je velikost wchar_t med prevajanjem tudi 2 bajta, vendar potem lahko pozabite na uporabo standardne knjižnice iz prevajalnika, ker uporablja glibc, ki je nato preveden za 4-bajtni wchar_t. Drugi razlogi so bili boljša implementacija standardnih razredov, podpora za zgoščene tabele in celo emulacija semantike premikanja znotraj vsebnikov, ki smo jo aktivno uporabljali. In razlog več, kot pravijo nenazadnje, je bila izvedba godal. Imeli smo svojo uro za godala, saj... Zaradi specifike naše programske opreme se nizovne operacije uporabljajo zelo široko in za nas je to ključnega pomena.

Naš niz temelji na idejah za optimizacijo nizov, izraženih v začetku leta 2000 Andrej Aleksandrescu. Kasneje, ko je Alexandrescu delal pri Facebooku, so na njegov predlog v motorju Facebook uporabili vrstico, ki je delovala po podobnih principih (glej knjižnico norost).

Naša linija je uporabljala dve glavni tehnologiji optimizacije:

  1. Za kratke vrednosti se uporablja notranji medpomnilnik v samem objektu niza (ne zahteva dodatne dodelitve pomnilnika).
  2. Za vse druge se uporablja mehanika Kopiraj pri pisanju. Vrednost niza je shranjena na enem mestu, med dodeljevanjem/spreminjanjem pa se uporablja referenčni števec.

Da bi pospešili kompilacijo platforme, smo iz naše različice STLPort (ki je nismo uporabili) izključili izvajanje toka, kar nam je dalo približno 20 % hitrejše kompiliranje. Kasneje smo ga morali uporabljati omejeno Pospešek. Boost močno uporablja tok, zlasti v API-jih svojih storitev (na primer za beleženje), zato smo ga morali spremeniti, da odstranimo uporabo toka. To pa nam je otežilo prehod na nove različice Boosta.

Tretja pot

Pri prehodu na standard C++14 smo upoštevali naslednje možnosti:

  1. Nadgradite STLPort, ki smo ga spremenili na standard C++14. Možnost je zelo težka, saj... podpora za STLPort je bila ukinjena leta 2010 in vso njegovo kodo bi morali sestaviti sami.
  2. Prehod na drugo izvedbo STL, združljivo s C++14. Zelo zaželeno je, da je ta izvedba za Windows in Linux.
  3. Pri prevajanju za vsak operacijski sistem uporabite knjižnico, vgrajeno v ustreznem prevajalniku.

Prvo možnost so zaradi prevelikega dela v celoti zavrnili.

Nekaj ​​časa smo razmišljali o drugi možnosti; velja za kandidata libc++, vendar takrat ni deloval pod Windows. Za prenos libc++ v Windows bi morali opraviti veliko dela - na primer, sami napisati vse, kar je povezano z nitmi, sinhronizacijo niti in atomičnostjo, saj se libc++ uporablja na teh področjih API POSIX.

In izbrali smo tretjo pot.

Prehod

Zato smo morali zamenjati uporabo STLPort s knjižnicami ustreznih prevajalnikov (Visual Studio 2015 za Windows, gcc 7 za Linux, clang 8 za macOS).

Na srečo je bila naša koda napisana predvsem po smernicah in ni uporabljala najrazličnejših pametnih trikov, tako da je selitev na nove knjižnice potekala razmeroma gladko, s pomočjo skriptov, ki so zamenjale imena tipov, razredov, imenskih prostorov in vključitev v vir. datoteke. Selitev je vplivala na 10 izvornih datotek (od 000). wchar_t je zamenjal char14_t; smo se odločili opustiti uporabo wchar_t, ker char000_t zavzame 16 bajta na vseh operacijskih sistemih in ne pokvari združljivosti kode med Windows in Linux.

Bilo je nekaj majhnih dogodivščin. Na primer, v STLPortu je bilo mogoče iterator implicitno pretvoriti v kazalec na element in na nekaterih mestih v naši kodi je bilo to uporabljeno. V novih knjižnicah tega ni bilo več mogoče storiti, te odlomke je bilo treba analizirati in prepisati ročno.

Tako je selitev kode končana, koda je sestavljena za vse operacijske sisteme. Čas je za teste.

Testi po prehodu so pokazali padec zmogljivosti (ponekod do 20-30%) in povečanje porabe pomnilnika (do 10-15%) v primerjavi s staro različico kode. To je bilo predvsem posledica neoptimalne zmogljivosti standardnih nizov. Zato smo ponovno morali uporabiti lastno, nekoliko spremenjeno linijo.

Razkrita je bila tudi zanimiva lastnost implementacije vsebnikov v vgrajenih knjižnicah: prazna (brez elementov) std::map in std::set iz vgrajenih knjižnic dodeljujeta pomnilnik. In zaradi izvedbenih značilnosti se ponekod v kodi ustvari precej praznih vsebnikov te vrste. Standardni pomnilniški vsebniki so dodeljeni malo, za en korenski element, vendar se je za nas to izkazalo za kritično - v številnih scenarijih se je naša zmogljivost znatno zmanjšala in poraba pomnilnika se je povečala (v primerjavi s STLPortom). Zato smo v naši kodi zamenjali ti dve vrsti vsebnikov iz vgrajenih knjižnic z njuno implementacijo iz Boosta, kjer ti vsebniki niso imeli te funkcije, s čimer smo rešili problem z upočasnjevanjem in povečano porabo pomnilnika.

Kot se pogosto zgodi po obsežnih spremembah v velikih projektih, tudi prva ponovitev izvorne kode ni delovala brez težav in tu je še posebej prav prišla podpora za razhroščevanje iteratorjev v izvedbi Windows. Korak za korakom smo napredovali in do pomladi 2017 (verzija 8.3.11 1C:Enterprise) je bila migracija zaključena.

Rezultati

Prehod na standard C++14 nam je vzel približno 6 mesecev. Večino časa je na projektu delal en (vendar zelo visoko kvalificiran) razvijalec, v zaključni fazi pa so se pridružili predstavniki ekip, odgovornih za posamezna področja – uporabniški vmesnik, strežniška gruča, razvojna in administrativna orodja itd.

Prehod nam je močno poenostavil delo pri prehodu na najnovejše različice standarda. Tako je različica 1C:Enterprise 8.3.14 (v razvoju, izid predviden v začetku prihodnjega leta) že prenesen na standard C++17.

Po selitvi imajo razvijalci več možnosti. Če smo prej imeli lastno spremenjeno različico STL in en imenski prostor std, imamo zdaj standardne razrede iz vgrajenih knjižnic prevajalnika v imenskem prostoru std, v imenskem prostoru stdx - naše vrstice in vsebnike, optimizirane za naše naloge, v boostu - najnovejša različica boost. In razvijalec uporablja tiste razrede, ki so optimalno primerni za reševanje njegovih težav.

Pri razvoju pomaga tudi »domača« implementacija konstruktorjev premika (premikanje konstruktorjev) za več razredov. Če ima razred konstruktor premika in je ta razred postavljen v vsebnik, potem STL optimizira kopiranje elementov znotraj vsebnika (na primer, ko je vsebnik razširjen in je treba spremeniti kapaciteto in prerazporediti pomnilnik).

Letenje v mazilu

Morda najbolj neprijetna (vendar ne kritična) posledica selitve je ta, da se soočamo s povečanjem količine datoteke obj, celoten rezultat gradnje z vsemi vmesnimi datotekami pa je začel zavzemati 60–70 GB. To vedenje je posledica posebnosti sodobnih standardnih knjižnic, ki so postale manj kritične do velikosti ustvarjenih servisnih datotek. To ne vpliva na delovanje prevedene aplikacije, povzroča pa številne nevšečnosti pri razvoju, zlasti podaljša čas prevajanja. Povečujejo se tudi zahteve po prostem disku na strežnikih za gradnjo in na strojih za razvijalce. Naši razvijalci delajo na več različicah platforme vzporedno in stotine gigabajtov vmesnih datotek včasih povzročajo težave pri njihovem delu. Težava je neprijetna, a ni kritična, njeno rešitev smo zaenkrat preložili. Tehnologijo obravnavamo kot eno od možnosti za rešitev graditi enotnost (zlasti Google ga uporablja pri razvoju brskalnika Chrome).

Vir: www.habr.com

Dodaj komentar