Qualche tempo fa (autunno 2016), durante lo sviluppo di una nuova versione della piattaforma tecnologica 1C:Enterprise, nel team di sviluppo si è posto il problema del supporto al nuovo standard nel nostro codice. Il passaggio al nuovo standard, come supponevamo, ci avrebbe permesso di scrivere molte cose in modo più elegante, semplice e sicuro, semplificando la manutenzione e il supporto del codice. E in termini di traduzione, sembrerebbe non esserci nulla di straordinario, se non fosse per le dimensioni della base di codice e le specifiche caratteristiche del nostro codice.
Per chi non lo sapesse, 1C:Enterprise è un ambiente per lo sviluppo rapido di applicazioni aziendali cross-platform e runtime per la loro esecuzione su diversi sistemi operativi e DBMS. In linea generale, il prodotto comprende:
- , opera su Windows e Linux
- , in comunicazione con il server tramite http(s) o tramite un proprio protocollo binario, opera su Windows, Linux, macOS
- , funziona nei browser Chrome, Internet Explorer, Microsoft Edge, Firefox, Safari (scritto in JavaScript)
- Ambiente di sviluppo (), funziona su Windows, Linux, macOS
- per server applicativi, funzionano su Windows, Linux, macOS
- , connesso al server tramite http(s), funziona su dispositivi mobili con Android, iOS, Windows
- — framework per la creazione di applicazioni mobili offline con possibilità di sincronizzazione, funzionano su Android, iOS, Windows
- Ambiente di sviluppo , scritto in Java
- Server
Cerchiamo di scrivere il codice il più possibile in modo uniforme per diversi sistemi operativi — la base di codice del server è comune al 99%, quella del client è di circa il 95%. La piattaforma tecnologica 1C:Enterprise è prevalentemente 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 portato su C++14. Oggi racconteremo come lo abbiamo fatto e quali sfide abbiamo affrontato nel processo.

Disclaimer
Tutto ciò che è scritto qui sotto riguardo al lavoro lento/veloce, (non)consumo di memoria delle implementazioni delle classi standard in varie 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à: abbiamo preso dati tipici dei nostri clienti, li abbiamo elaborati attraverso scenari tipici, abbiamo valutato le prestazioni, l'uso di memoria, ecc., e abbiamo analizzato se questi risultati fossero soddisfacenti o meno per noi e per i nostri clienti. E ci siamo comportati di conseguenza.
Cosa avevamo
Inizialmente scrivevamo il codice della piattaforma 1C:Enterprise 8 su Microsoft Visual Studio. Il progetto è stato avviato all'inizio degli anni 2000 e avevamo solo una versione per Windows. Naturalmente, da allora il codice si è sviluppato attivamente, molti meccanismi sono stati completamente riscritti. Ma il codice è stato scritto secondo lo standard del 1998, e, per esempio, le parentesi angolari destra erano separate da spazi per consentire una compilazione riuscita, in questo modo:
vector<vector > IntV;Nel 2006, con il rilascio della versione della piattaforma 8.1, abbiamo iniziato a supportare Linux e abbiamo adottato una libreria standard di terze parti . Una delle ragioni del passaggio era la gestione delle 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 4 byte per impostazione predefinita. Questo ha portato a incompatibilità dei 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 durante la compilazione sia di 2 byte, ma a quel punto ci si può scordare dell'uso della libreria standard del compilatore, in quanto utilizza glibc, la quale è stata compilata per wchar_t a 4 byte. Ulteriori motivi erano una realizzazione di qualità superiore delle classi standard, supporto per le tabelle hash e persino l'emulazione della semantica del movimento all'interno dei contenitori, che utilizzavamo attivamente. E un'ulteriore ragione, come si suol dire, last but not least, era la performance delle stringhe. Avevamo la nostra classe per le stringhe, poiché a causa della specificità del nostro software, le operazioni su stringhe vengono utilizzate molto ampiamente e per noi è critico.
La nostra stringa si basa sulle idee di ottimizzazione delle stringhe espresse già all'inizio degli anni 2000 . In seguito, quando Alexandrescu lavorava per Facebook, su sua iniziativa nel motore di Facebook è stata utilizzata una stringa che funziona su principi simili (vedi la libreria ).
. Nella nostra stringa sono state utilizzate due tecnologie principali di ottimizzazione:
- Per i valori brevi viene utilizzato un buffer interno all'oggetto stringa (che non richiede ulteriori allocazioni di memoria).
- Per tutti gli altri viene utilizzata la meccanica . Il valore della stringa è memorizzato in un unico posto, durante l'assegnazione/modifica viene utilizzato un contatore dei riferimenti.
Per accelerare la compilazione della piattaforma, abbiamo escluso dalla nostra versione di STLPort l'implementazione dello stream (che non utilizzavamo), il che ci ha dato un'accelerazione della compilazione di circa il 20%. Successivamente, ci siamo trovati a dover utilizzare in modo limitato . Boost utilizza attivamente lo stream, in particolare, nelle sue API di servizio (ad esempio, per il logging), quindi ci è toccato modificarlo, escludendo l'uso dello stream. Questo, a sua volta, ci ha reso difficile il passaggio a nuove versioni di Boost.
La terza via
Nella transizione allo standard C++14 abbiamo considerato le seguenti opzioni:
- Alzare lo STLPort modificato allo standard C++14. L'opzione è molto complessa, poiché il supporto per STLPort è stato interrotto nel 2010 e avremmo dovuto alzare tutto il suo codice da soli.
- Passare a un'altra implementazione di STL compatibile con C++14. È fondamentale che questa implementazione funzioni sia su Windows che su Linux.
- Utilizzare, durante la compilazione, la libreria integrata nel corrispondente compilatore per ogni OS.
La prima opzione è stata scartata subito a causa del volume troppo grande di lavoro.
Abbiamo riflettuto per un po' sulla seconda opzione; come candidato abbiamo considerato , ma a quel tempo non funzionava su Windows. Per portare libc++ su Windows, sarebbe stato necessario fare un bel po' di lavoro — per esempio, scrivere noi stessi tutto ciò che riguarda i thread, la sincronizzazione dei thread e l'atomicità, poiché in libc++ in questi ambiti veniva utilizzato .
E abbiamo scelto la terza via.
Transizione
Quindi, dovevamo sostituire l'uso di STLPort con le librerie corrispondenti dei compilatori (Visual Studio 2015 per Windows, gcc 7 per Linux, clang 8 per macOS).
Fortunatamente, il nostro codice è stato scritto principalmente seguendo le linee guida e non ha utilizzato trucchi complicati, 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, namespace e inclusioni. La migrazione ha riguardato 10.000 file sorgente (su 14.000). wchar_t è stato sostituito con char16_t; abbiamo deciso di rinunciare all'uso di 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 gli iteratori potevano essere implicitamente convertiti in puntatori agli elementi, e in alcune parti del nostro codice ciò è stato utilizzato. Nelle nuove librerie questo non era più possibile, e questi punti hanno dovuto essere analizzati e riscritti manualmente.
Quindi, la migrazione del codice è completata, il codice è compilato per tutti i sistemi operativi. È tempo di test.
I test dopo la transizione hanno mostrato un calo delle prestazioni (fino al 20-30% in alcuni casi) e un aumento dell'uso della memoria (fino al 10-15%) rispetto alla vecchia versione del codice. Questo era, in particolare, dovuto al funzionamento non ottimale delle stringhe standard. Pertanto, abbiamo dovuto utilizzare nuovamente la nostra stringa, leggermente modificata.
È emersa anche un'interessante caratteristica dell'implementazione dei contenitori nelle librerie incorporate: std::map e std::set vuoti (senza elementi) delle librerie incorporate allocano memoria. E noi, a causa delle particolarità della nostra implementazione, creiamo in alcune parti del codice un numero considerevole di contenitori vuoti di questo tipo. I contenitori standard allocano una quantità minima di memoria, per un singolo elemento radice, ma per noi questo si è rivelato critico: in alcuni scenari la nostra prestazione è notevolmente diminuita e il consumo di memoria è aumentato (rispetto a STLPort). Pertanto, abbiamo sostituito nel nostro codice questi due tipi di contenitori delle librerie incorporate con la loro implementazione di Boost, dove tali contenitori non avevano questa caratteristica, e questo ha risolto il problema legato al rallentamento e all'aumento del consumo di memoria.
Come spesso accade dopo cambiamenti su larga scala in grandi progetti, la prima iterazione dei sorgenti non ha funzionato senza problemi, e in questo ci è stata molto utile, in particolare, la supporto degli iteratori di debug nell'implementazione per Windows. Passo dopo passo, ci siamo mossi avanti, e nella primavera del 2017 (versione 8.3.11 di 1C:Enterprise) la migrazione è stata completata.
Conclusioni
Il passaggio allo standard C++14 ha richiesto circa 6 mesi. La maggior parte del tempo, un solo sviluppatore (molto qualificato) ha lavorato sul progetto, e nella fase finale sono stati coinvolti rappresentanti dei team responsabili di aree specifiche — UI, cluster di server, strumenti di sviluppo e amministrazione, ecc.
Il passaggio ha notevolmente semplificato il nostro lavoro di migrazione alle versioni più recenti dello standard. Ad esempio, la versione 1C:Enterprise 8.3.14 (in fase di sviluppo, il rilascio è previsto per l'inizio del prossimo anno) è già stata convertita allo standard .
Dopo la migrazione, gli sviluppatori hanno avuto a disposizione maggiori opportunità. Se prima avevamo una nostra versione modificata dello STL e uno spazio dei nomi std, ora nello spazio dei nomi std ci sono classi standard dalle librerie integrate del compilatore, nello spazio dei nomi stdx si trovano le nostre stringhe e contenitori ottimizzati per i nostri compiti, e in boost – una versione aggiornata di boost. Gli sviluppatori possono utilizzare le classi che meglio si adattano alla risoluzione dei loro compiti.
Aiuta nello sviluppo anche la "nativa" implementazione dei costruttori di spostamento () per diverse classi. Se una classe ha un costruttore di spostamento e questa classe viene inserita in un contenitore, allora STL ottimizza la copia degli elementi all'interno del contenitore (ad esempio, quando il contenitore si espande e deve cambiare capacity e riallocare la memoria).
Un aspetto negativo
Uno degli effetti più fastidiosi (ma non critici) della migrazione è stato l'aumento del volume , e il risultato completo della build con tutti i file intermedi è diventato di circa 60–70 GB. Questo comportamento è legato alle caratteristiche delle moderne librerie standard, che tendono a non essere più così critiche riguardo alla quantità di file di servizio generati. Ciò non influisce sul funzionamento dell'applicazione compilata, ma crea alcuni disagi nello sviluppo, in particolare aumenta il tempo di compilazione. Aumentano anche i requisiti di spazio libero su disco nei server di build e sui computer degli sviluppatori. I nostri sviluppatori lavorano contemporaneamente su più versioni della piattaforma, e centinaia di gigabyte di file intermedi creano a volte difficoltà nel lavoro. Il problema è sgradevole, ma non critico; abbiamo per ora rimandato la sua risoluzione. Come una delle opzioni per risolverlo, stiamo considerando la tecnica (in particolare, lo utilizza Google nello sviluppo del browser Chrome).
Fonte: habr.com
