Hur vi översatte 10 miljoner rader C++-kod till C++14-standarden (och sedan till C++17)

För en tid sedan (hösten 2016), under utvecklingen av nästa version av teknikplattformen 1C:Enterprise, uppstod frågan inom utvecklingsteamet om att stödja den nya standarden C ++ 14 i vår kod. Övergången till en ny standard, som vi antog, skulle tillåta oss att skriva många saker mer elegant, enkelt och tillförlitligt, och skulle förenkla support och underhåll av koden. Och det verkar inte finnas något extraordinärt i översättningen, om inte för skalan på kodbasen och de specifika egenskaperna hos vår kod.

För de som inte vet är 1C:Enterprise en miljö för snabb utveckling av plattformsoberoende affärsapplikationer och körtid för deras exekvering på olika operativsystem och DBMS. Generellt sett innehåller produkten:

  • Application Server Cluster, körs på Windows och Linux
  • kund, arbetar med servern via http(s) eller sitt eget binära protokoll, fungerar på Windows, Linux, macOS
  • Webbklient, körs i webbläsare Chrome, Internet Explorer, Microsoft Edge, Firefox, Safari (skrivet i JavaScript)
  • Utvecklingsmiljö (Konfigurator), fungerar på Windows, Linux, macOS
  • Administrationsverktyg applikationsservrar, körs på Windows, Linux, macOS
  • Mobil klient, ansluter till servern via http(s), fungerar på mobila enheter som kör Android, iOS, Windows
  • Mobil plattform — ett ramverk för att skapa offline mobilapplikationer med möjligheten att synkronisera, körs på Android, iOS, Windows
  • Utvecklingsmiljö 1C:Utvecklingsverktyg för företag, skrivet på Java
  • Server Interaktionssystem

Vi försöker skriva samma kod för olika operativsystem så mycket som möjligt - serverns kodbas är 99% vanlig, klientkodbasen är cirka 95%. Teknikplattformen 1C:Enterprise är huvudsakligen skriven i C++ och ungefärliga kodegenskaper anges nedan:

  • 10 miljoner rader C++-kod,
  • 14 tusen filer,
  • 60 tusen klasser,
  • en halv miljon metoder.

Och allt detta måste översättas till C++14. Idag kommer vi att berätta hur vi gjorde detta och vad vi stötte på under processen.

Hur vi översatte 10 miljoner rader C++-kod till C++14-standarden (och sedan till C++17)

varning

Allt som skrivs nedan om långsamt/snabbt arbete, (inte) stor minnesförbrukning vid implementeringar av standardklasser i olika bibliotek betyder en sak: detta är sant FÖR OSS. Det är mycket möjligt att standardimplementationer kommer att vara bäst lämpade för dina uppgifter. Vi utgick från våra egna uppgifter: vi tog data som var typiska för våra kunder, körde typiska scenarier på dem, tittade på prestanda, mängden minne som förbrukats etc., och analyserade om vi och våra kunder var nöjda med sådana resultat eller inte . Och de agerade beroende på.

Vad vi hade

Till en början skrev vi koden för 1C:Enterprise 8-plattformen med hjälp av Microsoft Visual Studio. Projektet startade i början av 2000-talet och vi hade en endast Windows-version. Naturligtvis, sedan dess har koden aktivt utvecklats, många mekanismer har skrivits om helt. Men koden skrevs enligt 1998 års standard, och till exempel var våra rätvinkliga parenteser åtskilda av mellanslag så att kompileringen skulle lyckas, så här:

vector<vector<int> > IntV;

2006, med lanseringen av plattformsversion 8.1, började vi stödja Linux och bytte till ett tredjeparts standardbibliotek STLPort. En av anledningarna till övergången var att arbeta med breda linjer. I vår kod använder vi std::wstring, som är baserad på typen wchar_t, genomgående. Dess storlek i Windows är 2 byte, och i Linux är standarden 4 byte. Detta ledde till inkompatibilitet mellan våra binära protokoll mellan klient och server, såväl som olika beständiga data. Med hjälp av gcc-alternativen kan du ange att storleken på wchar_t under kompilering också är 2 byte, men då kan du glömma att använda standardbiblioteket från kompilatorn, eftersom den använder glibc, som i sin tur är kompilerad för en 4-byte wchar_t. Andra skäl var bättre implementering av standardklasser, stöd för hashtabeller och till och med emulering av semantiken för att flytta inuti behållare, vilket vi aktivt använde. Och ytterligare en anledning, som de säger sist men inte minst, var stråkspel. Vi hade en egen klass för stråkar, för... På grund av vår programvaras särdrag används strängoperationer mycket brett och för oss är detta avgörande.

Vår sträng är baserad på strängoptimeringsidéer som uttrycktes i början av 2000-talet Andrei Alexandrescu. Senare, när Alexandrescu arbetade på Facebook, på hans förslag, användes en rad i Facebook-motorn som fungerade på liknande principer (se biblioteket dårskap).

Vår linje använde två huvudsakliga optimeringstekniker:

  1. För korta värden används en intern buffert i själva strängobjektet (kräver inte ytterligare minnesallokering).
  2. För alla andra används mekanik Kopiera på Skriv. Strängvärdet lagras på ett ställe och en referensräknare används vid tilldelning/ändring.

För att påskynda plattformskompileringen uteslöt vi streamimplementeringen från vår STLPort-variant (som vi inte använde), detta gav oss cirka 20% snabbare kompilering. Därefter var vi tvungna att göra begränsad användning Boost. Boost använder ström mycket, särskilt i sina tjänste-API:er (till exempel för loggning), så vi var tvungna att ändra den för att ta bort användningen av ström. Detta gjorde det i sin tur svårt för oss att migrera till nya versioner av Boost.

tredje vägen

När vi flyttade till C++14-standarden övervägde vi följande alternativ:

  1. Uppgradera STLPort vi modifierade till C++14-standarden. Alternativet är mycket svårt, eftersom... stödet för STLPort avbröts 2010, och vi skulle behöva bygga all dess kod själva.
  2. Övergång till en annan STL-implementering som är kompatibel med C++14. Det är mycket önskvärt att denna implementering är för Windows och Linux.
  3. När du kompilerar för varje operativsystem, använd biblioteket som är inbyggt i motsvarande kompilator.

Det första alternativet avvisades direkt på grund av för mycket arbete.

Vi funderade på det andra alternativet ett tag; betraktas som en kandidat libc++, men vid den tiden fungerade det inte under Windows. För att porta libc++ till Windows skulle du behöva göra mycket arbete - till exempel skriva allt själv som har med trådar, trådsynkronisering och atomicitet att göra, eftersom libc++ används i dessa områden POSIX API.

Och vi valde den tredje vägen.

övergång

Så vi var tvungna att ersätta användningen av STLPort med biblioteken för motsvarande kompilatorer (Visual Studio 2015 för Windows, gcc 7 för Linux, clang 8 för macOS).

Lyckligtvis skrevs vår kod huvudsakligen enligt riktlinjer och använde inte alla möjliga smarta knep, så migreringen till nya bibliotek gick relativt smidigt, med hjälp av skript som ersatte namnen på typer, klasser, namnutrymmen och inkluderar i källan filer. Migreringen påverkade 10 000 källfiler (av 14 000). wchar_t ersattes av char16_t; vi bestämde oss för att överge användningen av wchar_t, eftersom char16_t tar 2 byte på alla operativsystem och förstör inte kodkompatibiliteten mellan Windows och Linux.

Det blev några små äventyr. Till exempel, i STLPort kunde en iterator implicit castas till en pekare till ett element, och på vissa ställen i vår kod användes detta. I nya bibliotek var det inte längre möjligt att göra detta, och dessa avsnitt måste analyseras och skrivas om manuellt.

Så kodmigreringen är klar, koden kompileras för alla operativsystem. Det är dags för tester.

Tester efter övergången visade en nedgång i prestanda (på vissa ställen upp till 20-30%) och en ökning av minnesförbrukning (upp till 10-15%) jämfört med den gamla versionen av koden. Detta berodde i synnerhet på den suboptimala prestandan hos standardsträngar. Därför fick vi återigen använda vår egen, något modifierade linje.

Ett intressant inslag i implementeringen av behållare i inbäddade bibliotek avslöjades också: tomma (utan element) std::map och std::set från inbyggda bibliotek allokerar minne. Och på grund av implementeringsfunktionerna skapas på vissa ställen i koden ganska många tomma behållare av denna typ. Standardminnesbehållare tilldelas lite, för ett rotelement, men för oss visade det sig vara avgörande - i ett antal scenarier sjönk vår prestanda avsevärt och minnesförbrukningen ökade (jämfört med STLPort). Därför ersatte vi i vår kod dessa två typer av behållare från de inbyggda biblioteken med deras implementering från Boost, där dessa behållare inte hade denna funktion, och detta löste problemet med avmattning och ökad minnesförbrukning.

Som ofta sker efter storskaliga förändringar i stora projekt fungerade inte den första iterationen av källkoden utan problem, och här kom framför allt stöd för felsökning av iteratorer i Windows-implementeringen väl till pass. Steg för steg gick vi framåt, och till våren 2017 (version 8.3.11 1C:Enterprise) var migreringen klar.

Resultat av

Övergången till C++14-standarden tog oss cirka 6 månader. För det mesta arbetade en (men mycket högt kvalificerad) utvecklare med projektet, och i slutskedet anslöt sig representanter för team med ansvar för specifika områden - UI, serverkluster, utvecklings- och administrationsverktyg etc.

Övergången förenklade vårt arbete med att migrera till de senaste versionerna av standarden avsevärt. Således har version 1C:Enterprise 8.3.14 (under utveckling, släpps planerad till början av nästa år) redan överförts till standarden C++17.

Efter migreringen har utvecklarna fler alternativ. Om vi ​​tidigare hade vår egen modifierade version av STL och ett std-namnområde, nu har vi standardklasser från de inbyggda kompilatorbiblioteken i std-namnutrymmet, i stdx-namnområdet - våra linjer och behållare optimerade för våra uppgifter, i uppsving - den senaste versionen av boost. Och utvecklaren använder de klasser som är optimalt lämpade för att lösa hans problem.

Den "native" implementeringen av rörelsekonstruktörer hjälper också till i utvecklingen (flytta konstruktörer) för ett antal klasser. Om en klass har en flyttkonstruktor och denna klass placeras i en container, optimerar STL:n kopieringen av element inuti containern (till exempel när containern expanderas och det är nödvändigt att ändra kapacitet och omfördela minne).

Hake

Den kanske mest obehagliga (men inte kritiska) konsekvensen av migration är att vi står inför en ökning av volymen obj filer, och det fullständiga resultatet av bygget med alla mellanfiler började ta upp 60–70 GB. Detta beteende beror på särdragen hos moderna standardbibliotek, som har blivit mindre kritiska till storleken på genererade servicefiler. Detta påverkar inte driften av den kompilerade applikationen, men det orsakar ett antal olägenheter i utvecklingen, i synnerhet ökar kompileringstiden. Kraven på ledigt diskutrymme på byggservrar och på utvecklarmaskiner ökar också. Våra utvecklare arbetar på flera versioner av plattformen parallellt, och hundratals gigabyte med mellanfiler skapar ibland svårigheter i deras arbete. Problemet är obehagligt, men inte kritiskt, vi har skjutit upp lösningen tills vidare. Vi överväger teknik som ett av alternativen för att lösa det enhet bygga (särskilt Google använder det när man utvecklar webbläsaren Chrome).

Källa: will.com

Lägg en kommentar