Hoe ons 10 miljoen reëls C++-kode vertaal het na die C++14-standaard (en dan na C++17)

'n Tyd gelede (in die herfs van 2016), tydens die ontwikkeling van die volgende weergawe van die 1C:Enterprise-tegnologieplatform, het die vraag binne die ontwikkelingspan ontstaan ​​oor die ondersteuning van die nuwe standaard C ++ 14 in ons kode. Die oorgang na 'n nuwe standaard, soos ons aangeneem het, sou ons in staat stel om baie dinge eleganter, eenvoudiger en betroubaarder te skryf, en sou die ondersteuning en instandhouding van die kode vereenvoudig. En daar blyk niks buitengewoon in die vertaling te wees nie, al is dit nie vir die skaal van die kodebasis en die spesifieke kenmerke van ons kode nie.

Vir diegene wat nie weet nie, 1C:Enterprise is 'n omgewing vir die vinnige ontwikkeling van kruisplatform besigheidstoepassings en looptyd vir die uitvoering daarvan op verskillende bedryfstelsels en DBBS'e. In algemene terme bevat die produk:

Ons probeer soveel as moontlik dieselfde kode vir verskillende bedryfstelsels skryf - die bedienerkodebasis is 99% algemeen, die kliëntkodebasis is ongeveer 95%. Die 1C:Enterprise-tegnologieplatform is hoofsaaklik in C++ geskryf en benaderde kodekenmerke word hieronder gegee:

  • 10 miljoen reëls C++-kode,
  • 14 duisend lêers,
  • 60 duisend klasse,
  • 'n halfmiljoen metodes.

En al hierdie goed moes in C++14 vertaal word. Vandag sal ons jou vertel hoe ons dit gedoen het en wat ons in die proses teëgekom het.

Hoe ons 10 miljoen reëls C++-kode vertaal het na die C++14-standaard (en dan na C++17)

Vrywaring

Alles wat hieronder geskryf word oor stadige/vinnige werk, (nie) groot geheueverbruik deur implementering van standaardklasse in verskeie biblioteke beteken een ding: dit is waar VIR ONS. Dit is heel moontlik dat standaardimplementerings die beste geskik sal wees vir u take. Ons het van ons eie take begin: ons het data geneem wat tipies vir ons kliënte was, tipiese scenario's daarop uitgevoer, gekyk na prestasie, die hoeveelheid geheue wat verbruik word, ens., en ontleed of ons en ons kliënte tevrede is met sulke resultate of nie . En hulle het opgetree afhangende van.

Wat ons gehad het

Aanvanklik het ons die kode vir die 1C:Enterprise 8-platform in Microsoft Visual Studio geskryf. Die projek het in die vroeë 2000's begin en ons het 'n slegs Windows-weergawe gehad. Natuurlik, sedertdien is die kode aktief ontwikkel, baie meganismes is heeltemal herskryf. Maar die kode is volgens die 1998-standaard geskryf, en ons reghoekige hakies is byvoorbeeld deur spasies geskei sodat samestelling sou slaag, soos volg:

vector<vector<int> > IntV;

In 2006, met die vrystelling van platformweergawe 8.1, het ons Linux begin ondersteun en na 'n derdeparty-standaardbiblioteek oorgeskakel STLPort. Een van die redes vir die oorgang was om met wye lyne te werk. In ons kode gebruik ons ​​deurgaans std::wstring, wat gebaseer is op die wchar_t-tipe. Sy grootte in Windows is 2 grepe, en in Linux is die verstek 4 grepe. Dit het gelei tot onversoenbaarheid van ons binêre protokolle tussen kliënt en bediener, sowel as verskeie aanhoudende data. Deur die gcc opsies te gebruik, kan jy spesifiseer dat die grootte van wchar_t tydens samestelling ook 2 grepe is, maar dan kan jy vergeet om die standaard biblioteek van die samesteller te gebruik, want dit gebruik glibc, wat op sy beurt saamgestel is vir 'n 4-grepe wchar_t. Ander redes was beter implementering van standaardklasse, ondersteuning vir hash-tabelle, en selfs nabootsing van die semantiek van beweeg binne houers, wat ons aktief gebruik het. En nog 'n rede, soos hulle laaste maar nie die minste sê, was snaaruitvoering. Ons het ons eie klas vir snare gehad, want... As gevolg van die besonderhede van ons sagteware, word stringbewerkings baie wyd gebruik en vir ons is dit van kritieke belang.

Ons string is gebaseer op stringoptimaliseringsidees wat in die vroeë 2000's uitgedruk is Andrei Alexandrescu. Later, toe Alexandrescu by Facebook gewerk het, is op sy voorstel 'n lyn in die Facebook-enjin gebruik wat op soortgelyke beginsels gewerk het (sien biblioteek dwaasheid).

Ons lyn het twee hoofoptimeringstegnologieë gebruik:

  1. Vir kort waardes word 'n interne buffer in die stringvoorwerp self gebruik (wat nie bykomende geheuetoewysing vereis nie).
  2. Vir alle ander word meganika gebruik Kopieer op Skryf. Die stringwaarde word op een plek gestoor, en 'n verwysingsteller word tydens opdrag/wysiging gebruik.

Om platformsamestelling te bespoedig, het ons die stroomimplementering uitgesluit van ons STLPort-variant (wat ons nie gebruik het nie), dit het ons ongeveer 20% vinniger samestelling gegee. Daarna moes ons beperkte gebruik maak Boost. Boost maak baie gebruik van stroom, veral in sy diens-API's (byvoorbeeld, vir aanteken), so ons moes dit verander om die gebruik van stroom te verwyder. Dit het dit op sy beurt vir ons moeilik gemaak om na nuwe weergawes van Boost te migreer.

derde manier

Toe ons na die C++14-standaard beweeg het, het ons die volgende opsies oorweeg:

  1. Gradeer die STLPort op wat ons verander het na die C++14-standaard. Die opsie is baie moeilik, want... ondersteuning vir STLPort is in 2010 gestaak, en ons sal al sy kode self moet bou.
  2. Gaan oor na 'n ander STL-implementering wat versoenbaar is met C++14. Dit is hoogs wenslik dat hierdie implementering vir Windows en Linux is.
  3. Wanneer u vir elke bedryfstelsel saamstel, gebruik die biblioteek wat in die ooreenstemmende samesteller ingebou is.

Die eerste opsie is heeltemal afgekeur weens te veel werk.

Ons het geruime tyd aan die tweede opsie gedink; as 'n kandidaat oorweeg word libc++, maar op daardie tydstip het dit nie onder Windows gewerk nie. Om libc++ na Windows oor te dra, sal jy baie werk moet doen - skryf byvoorbeeld alles self wat met drade, draadsinchronisasie en atomiteit te doen het, aangesien libc++ in hierdie gebiede gebruik word POSIX API.

En ons het die derde manier gekies.

Oorgang

Dus, ons moes die gebruik van STLPort vervang met die biblioteke van die ooreenstemmende samestellers (Visual Studio 2015 vir Windows, gcc 7 vir Linux, clang 8 vir macOS).

Ons kode is gelukkig hoofsaaklik volgens riglyne geskryf en het nie allerhande slim truuks gebruik nie, dus het die migrasie na nuwe biblioteke relatief glad verloop, met behulp van skrifte wat die name van tipes, klasse, naamruimtes vervang het en in die bron insluit lêers. Die migrasie het 10 000 bronlêers (uit 14 000) geraak. wchar_t is vervang deur char16_t; ons het besluit om die gebruik van wchar_t te laat vaar, want char16_t neem 2 grepe op alle bedryfstelsels en bederf nie kodeversoenbaarheid tussen Windows en Linux nie.

Daar was 'n paar klein avonture. Byvoorbeeld, in STLPort kan 'n iterator implisiet na 'n wyser na 'n element gegooi word, en op sommige plekke in ons kode is dit gebruik. In nuwe biblioteke was dit nie meer moontlik om dit te doen nie, en hierdie gedeeltes moes met die hand ontleed en herskryf word.

Dus, die kode-migrasie is voltooi, die kode is saamgestel vir alle bedryfstelsels. Dit is tyd vir toetse.

Toetse ná die oorgang het 'n daling in werkverrigting getoon (op sommige plekke tot 20-30%) en 'n toename in geheueverbruik (tot 10-15%) in vergelyking met die ou weergawe van die kode. Dit was veral as gevolg van die suboptimale werkverrigting van standaard snare. Daarom moes ons weer ons eie, effens aangepaste lyn gebruik.

'n Interessante kenmerk van die implementering van houers in ingebedde biblioteke is ook aan die lig gebring: leë (sonder elemente) std::map en std::set vanaf ingeboude biblioteke ken geheue toe. En as gevolg van die implementeringskenmerke word op sommige plekke in die kode heelwat leë houers van hierdie tipe geskep. Standaardgeheuehouers word 'n bietjie vir een wortelelement toegewys, maar vir ons was dit van kritieke belang - in 'n aantal scenario's het ons werkverrigting aansienlik gedaal en geheueverbruik het toegeneem (in vergelyking met STLPort). Daarom het ons in ons kode hierdie twee soorte houers uit die ingeboude biblioteke vervang met hul implementering vanaf Boost, waar hierdie houers nie hierdie kenmerk gehad het nie, en dit het die probleem met verlangsaming en verhoogde geheueverbruik opgelos.

Soos dikwels gebeur na grootskaalse veranderinge in groot projekte, het die eerste iterasie van die bronkode nie sonder probleme gewerk nie, en veral hier het ondersteuning vir ontfouting-iterators in die Windows-implementering handig te pas gekom. Stap vir stap het ons vorentoe beweeg, en teen die lente van 2017 (weergawe 8.3.11 1C:Enterprise) is die migrasie voltooi.

Resultate van

Die oorgang na die C++14-standaard het ons ongeveer 6 maande geneem. Meeste van die tyd het een (maar baie hoogs gekwalifiseerde) ontwikkelaar aan die projek gewerk, en in die finale stadium het verteenwoordigers van spanne verantwoordelik vir spesifieke areas bygekom - UI, bedienerkluster, ontwikkelings- en administrasiehulpmiddels, ens.

Die oorgang het ons werk om na die nuutste weergawes van die standaard te migreer aansienlik vereenvoudig. Dus, weergawe 1C:Enterprise 8.3.14 (in ontwikkeling, vrystelling geskeduleer vir vroeg volgende jaar) is reeds na die standaard oorgedra C++17.

Na die migrasie het ontwikkelaars meer opsies. As ons vroeër ons eie gewysigde weergawe van STL en een std naamruimte gehad het, het ons nou standaard klasse van die ingeboude samesteller biblioteke in die std naamruimte, in die stdx naamruimte - ons lyne en houers geoptimaliseer vir ons take, in hupstoot - die nuutste weergawe van boost. En die ontwikkelaar gebruik daardie klasse wat optimaal geskik is om sy probleme op te los.

Die "inheemse" implementering van skuifkonstrukteurs help ook met ontwikkeling (skuif konstrukteurs) vir 'n aantal klasse. As 'n klas 'n skuifkonstruktor het en hierdie klas word in 'n houer geplaas, dan optimaliseer die STL die kopiëring van elemente binne die houer (byvoorbeeld wanneer die houer uitgebrei word en dit nodig is om kapasiteit te verander en geheue te hertoewys).

'n Lepel teer

Miskien is die mees onaangename (maar nie kritieke) gevolg van migrasie dat ons gekonfronteer word met 'n toename in die volume obj lêers, en die volledige resultaat van die bou met al die tussenlêers het 60–70 GB begin opneem. Hierdie gedrag is te wyte aan die eienaardighede van moderne standaardbiblioteke, wat minder krities geword het oor die grootte van gegenereerde dienslêers. Dit beïnvloed nie die werking van die saamgestelde toepassing nie, maar dit veroorsaak 'n aantal ongerief in ontwikkeling, veral, dit verhoog die samestellingstyd. Die vereistes vir vrye skyfspasie op boubedieners en op ontwikkelaarmasjiene neem ook toe. Ons ontwikkelaars werk op verskeie weergawes van die platform in parallel, en honderde gigagrepe van intermediêre lêers skep soms probleme in hul werk. Die probleem is onaangenaam, maar nie kritiek nie; ons het die oplossing daarvan vir eers uitgestel. Ons oorweeg tegnologie as een van die opsies om dit op te los eenheid bou (Google gebruik dit veral wanneer die Chrome-blaaier ontwikkel word).

Bron: will.com

Voeg 'n opmerking