Com hem traduït 10 milions de línies de codi C++ a l'estàndard C++14 (i després a C++17)

Fa un temps (a la tardor de 2016), durant el desenvolupament de la propera versió de la plataforma tecnològica 1C:Enterprise, va sorgir la pregunta dins de l'equip de desenvolupament sobre donar suport al nou estàndard. C ++ 14 al nostre codi. La transició a un nou estàndard, tal com suposava, ens permetria escriure moltes coses de manera més elegant, senzilla i fiable, i simplificaria el suport i el manteniment del codi. I sembla que no hi ha res extraordinari en la traducció, si no és per l'escala de la base del codi i les característiques específiques del nostre codi.

Per a aquells que no ho sàpiguen, 1C:Enterprise és un entorn per al desenvolupament ràpid d'aplicacions empresarials multiplataforma i temps d'execució per a la seva execució en diferents sistemes operatius i DBMS. En termes generals, el producte conté:

Intentem escriure el mateix codi per a diferents sistemes operatius tant com sigui possible: la base de codi del servidor és comú al 99%, la base de codi del client és d'aproximadament el 95%. La plataforma tecnològica 1C:Enterprise està escrita principalment en C++ i les característiques aproximades del codi es donen a continuació:

  • 10 milions de línies de codi C++,
  • 14 mil fitxers,
  • 60 mil classes,
  • mig milió de mètodes.

I totes aquestes coses s'havien de traduir a C++14. Avui us explicarem com ho hem fet i què ens hem trobat en el procés.

Com hem traduït 10 milions de línies de codi C++ a l'estàndard C++14 (i després a C++17)

Exempció de responsabilitat

Tot el que s'escriu a continuació sobre el treball lent/ràpid, (no) gran consum de memòria per implementacions de classes estàndard en diverses biblioteques significa una cosa: això és cert PER A NOSALTRES. És molt possible que les implementacions estàndard siguin les més adequades per a les vostres tasques. Vam partir de les nostres pròpies tasques: vam agafar dades típiques dels nostres clients, vam executar-hi escenaris típics, vam analitzar el rendiment, la quantitat de memòria consumida, etc., i vam analitzar si nosaltres i els nostres clients estàvem satisfets amb aquests resultats o no. . I van actuar en funció.

El que teníem

Inicialment, vam escriure el codi per a la plataforma 1C:Enterprise 8 a Microsoft Visual Studio. El projecte va començar a principis dels anys 2000 i teníem una versió només per a Windows. Naturalment, des de llavors el codi s'ha desenvolupat activament, molts mecanismes s'han reescrit completament. Però el codi es va escriure d'acord amb l'estàndard de 1998 i, per exemple, els nostres claudàtors en angle recte estaven separats per espais perquè la compilació tingués èxit, així:

vector<vector<int> > IntV;

El 2006, amb el llançament de la versió 8.1 de la plataforma, vam començar a donar suport a Linux i vam canviar a una biblioteca estàndard de tercers. Port STL. Un dels motius de la transició va ser treballar amb línies amples. Al nostre codi, utilitzem std::wstring, que es basa en el tipus wchar_t. La seva mida a Windows és de 2 bytes, i a Linux el valor predeterminat és de 4 bytes. Això va provocar la incompatibilitat dels nostres protocols binaris entre client i servidor, així com diverses dades persistents. Utilitzant les opcions gcc, podeu especificar que la mida de wchar_t durant la compilació també sigui de 2 bytes, però després us podeu oblidar d'utilitzar la biblioteca estàndard del compilador, perquè utilitza glibc, que al seu torn es compila per a un wchar_t de 4 bytes. Altres motius van ser una millor implementació de classes estàndard, suport per a taules hash i fins i tot l'emulació de la semàntica de moure's dins dels contenidors, que vam utilitzar activament. I un motiu més, com diuen, per últim, però no menys important, va ser el rendiment de corda. Teníem la nostra pròpia classe de cordes, perquè... A causa de les especificitats del nostre programari, les operacions de cadena s'utilitzen molt àmpliament i per a nosaltres això és fonamental.

La nostra cadena es basa en idees d'optimització de cadena expressades a principis dels anys 2000 Andrei Alexandrescu. Més tard, quan Alexandrescu va treballar a Facebook, per suggeriment seu, es va utilitzar una línia al motor de Facebook que funcionava amb principis similars (vegeu la biblioteca la bogeria).

La nostra línia utilitza dues tecnologies d'optimització principals:

  1. Per a valors curts, s'utilitza una memòria intermèdia interna a l'objecte cadena en si (no requereix assignació de memòria addicional).
  2. Per a tots els altres, s'utilitza la mecànica Còpia en escriptura. El valor de la cadena s'emmagatzema en un sol lloc i s'utilitza un comptador de referència durant l'assignació/modificació.

Per accelerar la compilació de la plataforma, vam excloure la implementació del flux de la nostra variant STLPort (que no vam utilitzar), això ens va donar una compilació aproximadament un 20% més ràpida. Posteriorment vam haver de fer un ús limitat Estímul. Boost fa un gran ús del flux, especialment a les seves API de servei (per exemple, per al registre), de manera que hem hagut de modificar-lo per eliminar l'ús del flux. Això, al seu torn, ens va dificultar la migració a noves versions de Boost.

La tercera via

Quan vam passar a l'estàndard C++14, vam considerar les opcions següents:

  1. Actualitzeu l'STLPort que hem modificat a l'estàndard C++14. L'opció és molt difícil, perquè... El suport per a STLPort es va suspendre el 2010 i hauríem de construir tot el seu codi nosaltres mateixos.
  2. Transició a una altra implementació STL compatible amb C++14. És molt desitjable que aquesta implementació sigui per a Windows i Linux.
  3. Quan compileu per a cada sistema operatiu, utilitzeu la biblioteca integrada al compilador corresponent.

La primera opció va ser rebutjada directament per massa treball.

Vam pensar durant un temps en la segona opció; considerat com a candidat libc++, però en aquell moment no funcionava amb Windows. Per portar libc++ a Windows, hauríeu de fer molta feina; per exemple, escriviu tot el que tingui a veure amb els fils, la sincronització de fils i l'atomicitat, ja que libc++ s'utilitza en aquestes àrees. API POSIX.

I vam triar la tercera via.

Transició

Per tant, vam haver de substituir l'ús de STLPort per les biblioteques dels compiladors corresponents (Visual Studio 2015 per a Windows, gcc 7 per a Linux, clang 8 per a macOS).

Afortunadament, el nostre codi es va escriure principalment d'acord amb les directrius i no va utilitzar tot tipus de trucs intel·ligents, de manera que la migració a noves biblioteques va procedir amb relativa facilitat, amb l'ajuda d'scripts que substituïen els noms de tipus, classes, espais de noms i inclou a la font. Fitxers. La migració va afectar 10 fitxers font (d'un total de 000). wchar_t va ser substituït per char14_t; vam decidir abandonar l'ús de wchar_t, perquè char000_t pren 16 bytes en tots els sistemes operatius i no fa malbé la compatibilitat del codi entre Windows i Linux.

Hi va haver algunes petites aventures. Per exemple, a STLPort un iterador es podria llançar implícitament a un punter a un element, i en alguns llocs del nostre codi es va utilitzar. A les biblioteques noves ja no era possible fer-ho, i aquests passatges s'havien d'analitzar i reescriure manualment.

Per tant, la migració del codi s'ha completat, el codi es compila per a tots els sistemes operatius. És el moment de les proves.

Les proves posteriors a la transició van mostrar una caiguda del rendiment (en alguns llocs fins a un 20-30%) i un augment del consum de memòria (fins a un 10-15%) en comparació amb la versió antiga del codi. Això va ser, en particular, a causa del rendiment subòptim de les cordes estàndard. Per tant, vam haver de tornar a utilitzar la nostra pròpia línia lleugerament modificada.

També es va revelar una característica interessant de la implementació de contenidors en biblioteques incrustades: buit (sense elements) std::map i std::set des de les biblioteques integrades assignen memòria. I a causa de les característiques d'implementació, en alguns llocs del codi es creen molts contenidors buits d'aquest tipus. Els contenidors de memòria estàndard s'assignen una mica, per a un element arrel, però per a nosaltres això va resultar crític: en diversos escenaris, el nostre rendiment va baixar significativament i el consum de memòria va augmentar (en comparació amb STLPort). Per tant, al nostre codi vam substituir aquests dos tipus de contenidors de les biblioteques integrades per la seva implementació des de Boost, on aquests contenidors no tenien aquesta característica, i això va resoldre el problema amb la desacceleració i l'augment del consum de memòria.

Com passa sovint després de canvis a gran escala en projectes grans, la primera iteració del codi font no va funcionar sense problemes, i aquí, en particular, el suport per a iteradors de depuració a la implementació de Windows va ser útil. Pas a pas vam avançar i a la primavera de 2017 (versió 8.3.11 1C:Enterprise) la migració es va completar.

Resultats de

La transició a l'estàndard C++14 ens va portar uns 6 mesos. La majoria de les vegades, un desenvolupador (però molt qualificat) treballava en el projecte i, en l'etapa final, s'hi incorporaven representants d'equips responsables d'àrees específiques: interfície d'usuari, clúster de servidors, eines de desenvolupament i administració, etc.

La transició va simplificar molt el nostre treball de migració a les últimes versions de l'estàndard. Així, la versió 1C:Enterprise 8.3.14 (en desenvolupament, llançament previst per a principis de l'any vinent) ja s'ha transferit a l'estàndard C++17.

Després de la migració, els desenvolupadors tenen més opcions. Si abans teníem la nostra pròpia versió modificada de STL i un espai de noms std, ara tenim classes estàndard de les biblioteques del compilador integrades a l'espai de noms std, a l'espai de noms stdx -les nostres línies i contenidors optimitzats per a les nostres tasques, en boost - el darrera versió de boost. I el desenvolupador utilitza aquelles classes òptimes per resoldre els seus problemes.

La implementació "nativa" dels constructors de moviment també ajuda en el desenvolupament (moure els constructors) per a diverses classes. Si una classe té un constructor de moviment i aquesta classe es col·loca en un contenidor, aleshores l'STL optimitza la còpia d'elements dins del contenidor (per exemple, quan el contenidor s'amplia i cal canviar la capacitat i reassignar la memòria).

Mosca a la sopa

Potser la conseqüència més desagradable (però no crítica) de la migració és que ens trobem davant d'un augment del volum fitxers obj, i el resultat complet de la compilació amb tots els fitxers intermedis va començar a ocupar entre 60 i 70 GB. Aquest comportament es deu a les peculiaritats de les biblioteques estàndard modernes, que s'han tornat menys crítiques amb la mida dels fitxers de servei generats. Això no afecta el funcionament de l'aplicació compilada, però sí que provoca una sèrie d'inconvenients en el desenvolupament, en particular, augmenta el temps de compilació. Els requisits d'espai lliure en disc als servidors de compilació i a les màquines de desenvolupament també augmenten. Els nostres desenvolupadors treballen en diverses versions de la plataforma en paral·lel, i centenars de gigabytes de fitxers intermedis de vegades creen dificultats en el seu treball. El problema és desagradable, però no crític; de moment hem posposat la seva solució. Considerem la tecnologia com una de les opcions per solucionar-ho construcció d'unitat (en particular, Google l'utilitza quan desenvolupa el navegador Chrome).

Font: www.habr.com

Afegeix comentari