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

Qualche tempo fa (autunno 2016), durante lo sviluppo dell'ennesima versione della piattaforma tecnologica 1C:Enterprise, il team di sviluppo si è posto la questione del supporto per il nuovo standard C++14 nel nostro codice. Il passaggio al nuovo standard, come avevamo previsto, ci avrebbe permesso di scrivere molte cose in modo più elegante, semplice e affidabile, semplificando la manutenzione e la gestione del codice. E nella traduzione non sembrerebbe esserci nulla di straordinario, se non fosse per le dimensioni 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 business cross-platform e runtime per la loro esecuzione su diversi sistemi operativi e DBMS. In generale, il prodotto include:

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

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

E tutto questo doveva essere aggiornato a C++14. Oggi vi racconteremo come abbiamo fatto e con quali difficoltà ci siamo trovati.

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

Disclaimer

Tutto ciò che segue riguardo alla lentezza/rapidità operativa, (non) grande consumo di memoria delle implementazioni delle classi standard in diverse librerie significa una cosa: è valido PER NOI. È del tutto possibile che le implementazioni standard siano le più adatte per le vostre esigenze. Noi ci siamo basati sulle nostre necessità: utilizzavamo dati tipici dei nostri clienti, eseguivamo scenari tipici, esaminavamo le prestazioni, il consumo di memoria e simili, e analizzavamo se i risultati ci soddisfacevano, così come ai nostri clienti, oppure no, e agivamo di conseguenza.

Cosa avevamo

Inizialmente scrivevamo il codice della piattaforma 1C:Enterprise 8 su Microsoft Visual Studio. Il progetto è iniziato nei primi anni 2000 e avevamo una versione solo per Windows. Naturalmente, da allora il codice si è evoluto notevolmente, molti meccanismi sono stati completamente riscritti. Ma il codice era scritto secondo lo standard del 1998 e, ad esempio, le parentesi angolari destre erano separate da spazi per garantire la corretta compilazione, in questo modo:

vector<vector> IntV;

Nel 2006, con l'uscita della versione 8.1 della piattaforma, abbiamo iniziato a supportare Linux e siamo passati a una libreria standard di terze parti. STLPort. Una delle ragioni del passaggio è stata la gestione di stringhe larghe. Nel nostro codice utilizziamo ovunque std::wstring, basato sul tipo wchar_t. La sua dimensione in Windows è di 2 byte, mentre in Linux è di default 4 byte. Questo ha portato a incompatibilità nei nostri protocolli binari tra client e server, oltre a diversi dati persistenti. Con le opzioni di gcc è possibile specificare che la dimensione di wchar_t alla compilazione sia anch'essa di 2 byte, ma in tal caso si può dimenticare l'uso della libreria standard del compilatore, poiché questa utilizza glibc, che a sua volta è compilata per wchar_t a 4 byte. Altre ragioni includevano una migliore implementazione delle classi standard, il supporto per le tabelle hash e persino l'emulazione della semantica di spostamento all'interno dei contenitori, di cui abbiamo fatto ampio uso. E infine, ma non meno importante, c'era la performance delle stringhe. Avevamo la nostra classe per le stringhe, poiché nel nostro software, a causa delle sue specificità, le operazioni sulle stringhe sono utilizzate molto ampiamente e per noi questo è critico.

La nostra stringa è basata su idee di ottimizzazione delle stringhe enunciate all'inizio degli anni 2000. Andrei Alexandrescu. In seguito, quando Alexandrescu lavorava per Facebook, è stata implementata una stringa nel motore di Facebook che utilizza principi simili (vedi la libreria folly).

Nella nostra stringa abbiamo utilizzato due tecnologie principali di ottimizzazione:

  1. Per i valori brevi viene utilizzato un buffer interno all'oggetto stringa (senza necessità di ulteriore allocazione di memoria).
  2. Per tutti gli altri viene utilizzata la meccanica Copy On Write. Il valore della stringa è memorizzato in un unico luogo e, durante l'assegnazione/modifica, viene utilizzato un contatore di riferimenti.

Per accelerare la compilazione della piattaforma, abbiamo escluso dal nostro variante di STLPort l'implementazione dello stream (che non utilizzavamo), il che ci ha dato un'accelerazione della compilazione di circa il 20%. Successivamente, abbiamo dovuto utilizzare in modo limitato Boost. Boost utilizza attivamente lo stream, in particolare nei suoi API di servizio (ad esempio, per il logging), quindi ci siamo dovuti modificare, escludendo l'uso dello stream. Questo, a sua volta, ha complicato il nostro passaggio a nuove versioni di Boost.

Il terzo modo

Nella transizione allo standard C++14 abbiamo considerato opzioni come:

  1. Aggiornare STLPort modificato da noi allo standard C++14. Questa opzione è molto complessa, poiché il supporto per STLPort è stato interrotto nel 2010, e dovremmo gestire l'intero codice da soli.
  2. Passare a un'altra implementazione di STL compatibile con C++14. È altamente desiderabile che questa implementazione funzioni su Windows e Linux.
  3. Utilizzare la libreria integrata nel rispettivo compilatore per ciascun sistema operativo durante la compilazione.

La prima opzione è stata subito scartata a causa dell'elevato volume di lavoro richiesto.

Abbiamo riflettuto a lungo sulla seconda opzione; tra i candidati abbiamo considerato libc++, ma quel progetto non funzionava su Windows all'epoca. Portare libc++ su Windows richiederebbe un notevole lavoro — ad esempio, dovremmo scrivere noi stessi tutto ciò che riguarda i thread, la sincronizzazione dei thread e l'atomicità, poiché in libc++ veniva utilizzato l'API POSIX.

E abbiamo scelto la terza strada.

Transizione

Quindi, dovevamo sostituire l'uso 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 seguendo delle linee guida e non ha utilizzato trucchi strani, quindi la migrazione verso le nuove librerie è avvenuta in modo relativamente fluido, grazie a script che sostituivano nei file sorgente i nomi di tipi, classi, spazi dei nomi e inclusioni. La migrazione ha interessato 10.000 file sorgente (su 14.000). wchar_t è stato sostituito da char16_t; abbiamo scelto di abbandonare wchar_t poiché char16_t occupa 2 byte su tutti i sistemi operativi e non compromette la compatibilità del codice tra Windows e Linux.

Non sono mancati piccoli imprevisti. Ad esempio, in STLPort l'iteratore poteva essere convertito implicitamente in un puntatore all'elemento, e in alcuni punti del nostro codice questo veniva utilizzato. Nelle nuove librerie non era più possibile farlo, e questi punti dovevano essere analizzati e riscritti manualmente.

Quindi, la migrazione del codice è completata, il codice si compila per tutti i sistemi operativi. È tempo di test.

I test effettuati dopo la migrazione hanno mostrato una riduzione delle prestazioni (fino al 20-30% in alcuni casi) e un aumento della memoria utilizzata (fino al 10-15%) rispetto alla vecchia versione del codice. Questo era dovuto, in particolare, all'inefficienza delle stringhe standard. Pertanto, abbiamo dovuto utilizzare nuovamente la nostra versione leggermente modificata.

È emersa anche una caratteristica interessante dell'implementazione dei contenitori nelle librerie incorporate: i std::map e std::set vuoti (senza elementi) delle librerie standard allocano memoria. A causa delle peculiarità della nostra implementazione, in alcune parti del codice vengono creati molti contenitori vuoti di questo tipo. I contenitori standard allocano una quantità minima di memoria, solo per un elemento radice, ma per noi questo si è rivelato critico: in diversi scenari, abbiamo notato una sensibile riduzione delle prestazioni e un aumento del consumo di memoria (rispetto a STLPort). Pertanto, abbiamo sostituito questi due tipi di contenitori delle librerie standard con l'implementazione di Boost, nella quale tali contenitori non presentavano questa problematica, risolvendo così il problema della lentezza e dell'aumento del consumo di memoria.

Come spesso accade dopo cambiamenti significativi in grandi progetti, la prima iterazione del codice sorgente ha presentato non pochi problemi, e in questo ci è stata molto utile, in particolare, il supporto degli iteratori di debug nell'implementazione per Windows. Passo dopo passo, abbiamo continuato a progredire, e entro la primavera del 2017 (versione 8.3.11 di 1C:Enterprise) la migrazione era completata.

Risultati

Il passaggio allo standard C++14 ci ha preso circa 6 mesi. La maggior parte del tempo, un solo sviluppatore (ma molto qualificato) ha lavorato al progetto, e nella fase finale si sono uniti rappresentanti dei team responsabili per aree specifiche — UI, cluster di server, strumenti di sviluppo e amministrazione, ecc.

Il passaggio ha notevolmente semplificato il nostro lavoro di migrazione verso le versioni più recenti dello standard. Così, la versione 8.3.14 di 1C:Enterprise (in fase di sviluppo, il rilascio è programmato per all'inizio del prossimo anno) è già stata convertita allo standard. C++17.

Dopo la migrazione, i programmatori hanno a disposizione maggiori opportunità. Se prima avevamo una versione rivisitata di STL con uno spazio dei nomi std, ora nello spazio dei nomi std troviamo le classi standard delle librerie integrate del compilatore, nello spazio dei nomi stdx – le nostre stringhe e contenitori ottimizzati per i nostri compiti, in boost – la versione aggiornata di boost. E lo sviluppatore utilizza le classi che si adattano meglio per risolvere i suoi problemi.

Aiuta nello sviluppo anche la realizzazione "nativa" dei costruttori di spostamento (move constructors) per alcune classi. Se una classe ha un costruttore di spostamento e questa classe viene inserita in un contenitore, STL ottimizza la copia degli elementi all'interno del contenitore (ad esempio, quando il contenitore si espande e deve cambiare la capacità e riallocare la memoria).

Un cucchiaio di catrame

La conseguenza più spiacevole (ma non critica) della migrazione è che abbiamo riscontrato un aumento del volume dei file obj, e il risultato completo della build, con tutti i file di intermediazione, occupa tra 60 e 70 GB. Questo comportamento è legato alle caratteristiche delle moderne librerie standard, che tendono a non considerar più critico il volume dei file di servizio generati. Ciò non influisce sul funzionamento delle applicazioni compilate, ma crea alcuni inconvenienti nello sviluppo, in particolare aumentando i tempi di compilazione. Aumentano anche i requisiti per lo spazio libero su disco sui server di build e sulle macchine degli sviluppatori. I nostri sviluppatori stanno lavorando su più versioni della piattaforma contemporaneamente, e centinaia di gigabyte di file intermedi a volte creano difficoltà operative. Il problema è sgradevole, ma non critico; per ora abbiamo rimandato la sua risoluzione. Come una delle possibili soluzioni, stiamo considerando la tecnica unity build (questa, in particolare, è utilizzata da Google nello sviluppo del browser Chrome).

Fonte: habr.com

Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server 🔥 Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server | ProHoster