Come abbiamo tradotto 10 milioni di righe di codice C++ nello standard C++14 (e poi in C++17)

Qualche tempo fa (nell'autunno del 2016), durante lo sviluppo della prossima versione della piattaforma tecnologica 1C:Enterprise, all'interno del team di sviluppo è sorta la domanda sul supporto del nuovo standard C ++ 14 nel nostro codice. Il passaggio a un nuovo standard, come ipotizzato, ci consentirebbe di scrivere molte cose in modo più elegante, semplice e affidabile e semplificherebbe il supporto e la manutenzione del codice. E non sembra esserci nulla di straordinario nella traduzione, se non per la scala della base di codice e le caratteristiche specifiche del nostro codice.

Per chi non lo sapesse, 1C:Enterprise è un ambiente per lo sviluppo rapido di applicazioni aziendali multipiattaforma e runtime per la loro esecuzione su diversi sistemi operativi e DBMS. In termini generali, il prodotto contiene:

Cerchiamo di scrivere il più possibile lo stesso codice per sistemi operativi diversi: la codebase del server è comune al 99%, la codebase del client è circa il 95%. La piattaforma tecnologica 1C:Enterprise è scritta principalmente in C++ e le caratteristiche approssimative del codice sono fornite di seguito:

  • 10 milioni di righe di codice C++,
  • 14mila file,
  • 60mila lezioni,
  • mezzo milione di metodi.

E tutta questa roba doveva essere tradotta in C++14. Oggi vi diremo come abbiamo fatto questo e cosa abbiamo riscontrato nel processo.

Come abbiamo tradotto 10 milioni di righe di codice C++ nello standard C++14 (e poi in C++17)

Disclaimer

Tutto ciò che è scritto di seguito sul lavoro lento/veloce e sul (non) grande consumo di memoria da parte delle implementazioni di classi standard in varie librerie significa una cosa: questo è vero PER NOI. È del tutto possibile che le implementazioni standard siano più adatte ai tuoi compiti. Siamo partiti dai nostri compiti: abbiamo raccolto dati tipici dei nostri clienti, eseguito su di essi scenari tipici, osservato le prestazioni, la quantità di memoria consumata, ecc. e analizzato se noi e i nostri clienti eravamo soddisfatti o meno di tali risultati . E hanno agito a seconda.

Quello che avevamo

Inizialmente, abbiamo scritto il codice per la piattaforma 1C:Enterprise 8 utilizzando Microsoft Visual Studio. Il progetto è iniziato all'inizio degli anni 2000 e avevamo una versione solo per Windows. Naturalmente, da allora il codice è stato sviluppato attivamente, molti meccanismi sono stati completamente riscritti. Ma il codice è stato scritto secondo lo standard del 1998 e, ad esempio, le nostre parentesi uncinate sono state separate da spazi in modo che la compilazione riuscisse, in questo modo:

vector<vector<int> > IntV;

Nel 2006, con il rilascio della versione 8.1 della piattaforma, abbiamo iniziato a supportare Linux e siamo passati a una libreria standard di terze parti STLPort. Uno dei motivi della transizione è stato quello di lavorare con linee larghe. Nel nostro codice utilizziamo std::wstring, che si basa sempre sul tipo wchar_t. La sua dimensione in Windows è 2 byte e in Linux il valore predefinito è 4 byte. Ciò ha portato all'incompatibilità dei nostri protocolli binari tra client e server, nonché a vari dati persistenti. Usando le opzioni gcc, puoi specificare che anche la dimensione di wchar_t durante la compilazione è di 2 byte, ma poi puoi dimenticarti di usare la libreria standard del compilatore, perché utilizza glibc, che a sua volta è compilato per un wchar_t da 4 byte. Altri motivi sono stati una migliore implementazione delle classi standard, il supporto per le tabelle hash e persino l'emulazione della semantica dello spostamento all'interno dei contenitori, che abbiamo utilizzato attivamente. E un motivo in più, come si suol dire, ultimo ma non meno importante, era la performance delle corde. Avevamo il nostro corso per gli archi, perché... A causa delle specificità del nostro software, le operazioni sulle stringhe sono utilizzate molto ampiamente e per noi questo è fondamentale.

La nostra stringa si basa su idee di ottimizzazione delle stringhe espresse nei primi anni 2000 Andrei Alexandrescu. Più tardi, quando Alexandrescu lavorò su Facebook, su suo suggerimento, nel motore di Facebook fu usata una linea che funzionava su principi simili (vedi libreria follia).

La nostra linea utilizzava due principali tecnologie di ottimizzazione:

  1. Per valori brevi viene utilizzato un buffer interno nell'oggetto stringa stesso (che non richiede allocazione di memoria aggiuntiva).
  2. Per tutti gli altri si usa la meccanica Copia su Scrivi. Il valore della stringa viene archiviato in un'unica posizione e durante l'assegnazione/modifica viene utilizzato un contatore di riferimento.

Per accelerare la compilazione della piattaforma, abbiamo escluso l'implementazione dello stream dalla nostra variante STLPort (che non abbiamo utilizzato), questo ci ha dato una compilazione più veloce di circa il 20%. Successivamente abbiamo dovuto farne un uso limitato Potenzia. Boost fa un uso intensivo dello stream, in particolare nelle API dei suoi servizi (ad esempio, per la registrazione), quindi abbiamo dovuto modificarlo per rimuovere l'uso dello stream. Ciò, a sua volta, ci ha reso difficile la migrazione alle nuove versioni di Boost.

La terza via

Quando siamo passati allo standard C++14, abbiamo considerato le seguenti opzioni:

  1. Aggiorna la STLPort che abbiamo modificato allo standard C++14. L'opzione è molto difficile, perché... il supporto per STLPort è stato interrotto nel 2010 e avremmo dovuto creare da soli tutto il suo codice.
  2. Transizione a un'altra implementazione STL compatibile con C++14. È altamente auspicabile che questa implementazione sia per Windows e Linux.
  3. Durante la compilazione per ciascun sistema operativo, utilizzare la libreria incorporata nel compilatore corrispondente.

La prima opzione è stata completamente scartata a causa del troppo lavoro.

Pensavamo da tempo alla seconda opzione; considerato un candidato libc++, ma a quel tempo non funzionava con Windows. Per portare libc++ su Windows, dovresti fare molto lavoro, ad esempio scrivere tu stesso tutto ciò che ha a che fare con i thread, la sincronizzazione dei thread e l'atomicità, poiché libc++ viene utilizzato in queste aree API POSIX.

E abbiamo scelto la terza via.

transizione

Abbiamo quindi dovuto sostituire l'utilizzo di STLPort con le librerie dei compilatori corrispondenti (Visual Studio 2015 per Windows, gcc 7 per Linux, clang 8 per macOS).

Fortunatamente, il nostro codice è stato scritto principalmente secondo le linee guida e non ha utilizzato tutti i tipi di trucchi intelligenti, quindi la migrazione alle nuove librerie è avvenuta in modo relativamente fluido, con l'aiuto di script che hanno sostituito i nomi di tipi, classi, spazi dei nomi e include nel sorgente File. La migrazione ha interessato 10 file sorgente (su 000). wchar_t è stato sostituito da char14_t; abbiamo deciso di abbandonare l'uso di wchar_t, perché char000_t occupa 16 byte su tutti i sistemi operativi e non compromette la compatibilità del codice tra Windows e Linux.

Ci sono state alcune piccole avventure. Ad esempio, in STLPort un iteratore potrebbe essere implicitamente convertito in un puntatore a un elemento e in alcuni punti del nostro codice è stato utilizzato questo. Nelle nuove biblioteche ciò non era più possibile e questi passaggi dovevano essere analizzati e riscritti manualmente.

Quindi, la migrazione del codice è completa, il codice è compilato per tutti i sistemi operativi. E' il momento delle prove.

I test dopo la transizione hanno mostrato un calo delle prestazioni (in alcuni punti fino al 20-30%) e un aumento del consumo di memoria (fino al 10-15%) rispetto alla vecchia versione del codice. Ciò è dovuto in particolare alle prestazioni non ottimali delle corde standard. Pertanto, abbiamo dovuto utilizzare ancora una volta la nostra linea, leggermente modificata.

È stata anche rivelata una caratteristica interessante dell'implementazione dei contenitori nelle librerie integrate: vuoti (senza elementi) std::map e std::set dalle librerie integrate allocano memoria. E a causa delle funzionalità di implementazione, in alcuni punti del codice vengono creati molti contenitori vuoti di questo tipo. I contenitori di memoria standard vengono allocati un po', per un elemento root, ma per noi questo si è rivelato fondamentale: in una serie di scenari, le nostre prestazioni sono diminuite in modo significativo e il consumo di memoria è aumentato (rispetto a STLPort). Pertanto, nel nostro codice abbiamo sostituito questi due tipi di contenitori dalle librerie integrate con la loro implementazione da Boost, dove questi contenitori non avevano questa funzionalità, e questo ha risolto il problema del rallentamento e dell'aumento del consumo di memoria.

Come spesso accade dopo modifiche su larga scala in progetti di grandi dimensioni, la prima iterazione del codice sorgente non ha funzionato senza problemi e qui, in particolare, è tornato utile il supporto per il debug degli iteratori nell'implementazione di Windows. Passo dopo passo siamo andati avanti e nella primavera del 2017 (versione 8.3.11 1C:Enterprise) la migrazione è stata completata.

Risultati di

La transizione allo standard C++14 ha richiesto circa 6 mesi. Nella maggior parte dei casi, uno sviluppatore (ma molto qualificato) ha lavorato al progetto e nella fase finale si sono uniti i rappresentanti dei team responsabili di aree specifiche: interfaccia utente, cluster di server, strumenti di sviluppo e amministrazione, ecc.

La transizione ha notevolmente semplificato il nostro lavoro sulla migrazione alle versioni più recenti dello standard. Pertanto la versione 1C:Enterprise 8.3.14 (in sviluppo, rilascio previsto per l'inizio del prossimo anno) è già stata trasferita allo standard C++17.

Dopo la migrazione, gli sviluppatori hanno più opzioni. Se prima avevamo la nostra versione modificata di STL e uno spazio dei nomi std, ora abbiamo classi standard dalle librerie del compilatore integrate nello spazio dei nomi std, nello spazio dei nomi stdx - le nostre linee e contenitori ottimizzati per i nostri compiti, in boost - il ultima versione di boost. E lo sviluppatore utilizza quelle classi che sono perfettamente adatte a risolvere i suoi problemi.

L'implementazione "nativa" dei costruttori di mosse aiuta anche nello sviluppo (spostare i costruttori) per diverse classi. Se una classe ha un costruttore di spostamento e questa classe viene inserita in un contenitore, allora l'STL ottimizza la copia degli elementi all'interno del contenitore (ad esempio, quando il contenitore viene espanso ed è necessario modificare la capacità e riallocare la memoria).

Unico neo

Forse la conseguenza più spiacevole (ma non critica) della migrazione è che ci troviamo di fronte ad un aumento del volume file obje il risultato completo della compilazione con tutti i file intermedi ha iniziato a occupare 60-70 GB. Questo comportamento è dovuto alle peculiarità delle moderne librerie standard, che sono diventate meno critiche riguardo alla dimensione dei file di servizio generati. Ciò non pregiudica il funzionamento dell'applicazione compilata, ma provoca una serie di inconvenienti nello sviluppo, in particolare aumenta i tempi di compilazione. Aumentano anche i requisiti di spazio libero su disco sui server di build e sulle macchine degli sviluppatori. I nostri sviluppatori lavorano in parallelo su diverse versioni della piattaforma e centinaia di gigabyte di file intermedi a volte creano difficoltà nel loro lavoro. Il problema è spiacevole, ma non critico; per ora ne abbiamo rinviato la soluzione. Stiamo considerando la tecnologia come una delle opzioni per risolverlo costruire l'unità (in particolare Google lo utilizza durante lo sviluppo del browser Chrome).

Fonte: habr.com

Aggiungi un commento