Wie wir 10 Millionen Zeilen C++-Code auf den Standard C++14 (und dann auf C++17) übertragen haben

Vor einiger Zeit (Herbst 2016) stellte sich im Entwicklungsteam bei der Entwicklung einer neuen Version der technologischen Plattform 1C:Unternehmen die Frage nach der Unterstützung des neuen Standards C++14 in unserem Code. Der Umstieg auf den neuen Standard würde, wie wir annahmen, viele Dinge eleganter, einfacher und zuverlässiger machen und die Wartung und Pflege des Codes erleichtern. Und in der Übersetzung scheint es nichts Außergewöhnliches zu geben, wenn man nicht die Größenordnung der Codebasis und die spezifischen Eigenschaften unseres Codes berücksichtigt.

Für diejenigen, die es nicht wissen: 1C:Unternehmen ist eine Umgebung für die schnelle Entwicklung von plattformübergreifenden Geschäftsanwendungen und eine Runtime für deren Ausführung in verschiedenen Betriebssystemen und Datenbankmanagementsystemen. Im Allgemeinen umfasst das Produkt:

Wir versuchen, den größtmöglichen Teil des Codes für verschiedene Betriebssysteme zu schreiben — die Codebasis des Servers beträgt 99%, die des Clients etwa 95%. Die technologische Plattform 1C:Unternehmen ist überwiegend in C++ geschrieben, und hier sind die ungefähren Merkmale des Codes:

  • 10 Millionen Zeilen C++-Code,
  • 14.000 Dateien,
  • 60.000 Klassen,
  • eine halbe Million Methoden.

Und all das musste auf C++14 umgestellt werden. Heute werden wir darüber berichten, wie wir das gemacht haben und mit welchen Herausforderungen wir dabei konfrontiert wurden.

Wie wir 10 Millionen Zeilen C++-Code auf den Standard C++14 (und dann auf C++17) übertragen haben

Haftungsausschluss

Alles, was unten über die langsame/schnelle Leistung, den (nicht) geringen Speicherverbrauch von Implementierungen der Standardklassen in verschiedenen Bibliotheken geschrieben ist, bedeutet eines: Es gilt FÜR UNS. Es ist durchaus möglich, dass die Standardimplementierungen für Ihre Aufgaben am besten geeignet sind. Wir haben uns jedoch an unseren Aufgaben orientiert: Wir haben typische Daten unserer Kunden genommen, typische Szenarien damit durchlaufen, die Leistung, den Speicherverbrauch usw. betrachtet und analysiert, ob uns und unseren Kunden solche Ergebnisse akzeptabel sind oder nicht. Und wir haben entsprechend gehandelt.

Was wir hatten

Ursprünglich schrieben wir den Code der Plattform 1C:Enterprise 8 in Microsoft Visual Studio. Das Projekt begann Anfang der 2000er Jahre und wir hatten eine Version nur für Windows. Natürlich hat sich der Code seitdem aktiv entwickelt, viele Mechanismen wurden vollständig überarbeitet. Aber der Code wurde nach dem Standard von 1998 geschrieben, und beispielsweise waren die rechten spitzen Klammern bei uns durch Leerzeichen getrennt, damit die Kompilierung erfolgreich verlief, so:

vector<vector > IntV;

Im Jahr 2006, mit der Veröffentlichung der Version 8.1 der Plattform, begannen wir, Linux zu unterstützen und wechselten zu einer externen Standardbibliothek STLPort. Einer der Gründe für den Wechsel war die Arbeit mit breiten Zeichenfolgen. In unserem Code verwenden wir überall std::wstring, basierend auf dem Typ wchar_t. Seine Größe beträgt in Windows 2 Bytes, in Linux standardmäßig 4 Bytes. Dies führte zu Inkompatibilität unserer binären Protokolle zwischen Client und Server sowie zu verschiedenen persistenten Daten. Mit gcc-Optionen kann man angeben, dass die Größe von wchar_t bei der Kompilierung ebenfalls 2 Bytes betragen soll, aber dann kann man die Verwendung der Standardbibliothek des Compilers vergessen, da sie glibc verwendet und diese wiederum für 4-Byte-wchar_t kompiliert ist. Weitere Gründe waren eine qualitativ hochwertigere Implementierung der Standardklassen, Unterstützung von Hash-Tabellen und sogar die Emulation der Verschiebungssemantik innerhalb von Containern, die wir intensiv genutzt haben. Und ein weiterer Grund, wie man sagen könnte last but not least, war die Leistung von Zeichenfolgen. Wir hatten unsere eigene Klasse für Zeichenfolgen, da in Anbetracht der Spezifik unseres Softwares Zeichenoperationen sehr weit verbreitet sind und für uns kritische Bedeutung haben.

Unsere Zeichenfolge basiert auf den Ideen zur Optimierung von Zeichenfolgen, die bereits Anfang der 2000er Jahre geäußert wurden von Andrei Alexandrescu. Später, als Alexandrescu bei Facebook arbeitete, wurde auf seine Anregung hin eine Zeichenkette im Facebook-Engine verwendet, die nach ähnlichen Prinzipien funktionierte (siehe Bibliothek folly).

. In unserer Zeichenkette kamen zwei Hauptoptimierungstechnologien zum Einsatz:

  1. Für kurze Werte wird ein interner Puffer im Zeichenkettenobjekt verwendet (der keine zusätzliche Speicheralokation benötigt).
  2. Für alle anderen kommt die Mechanik Copy On Write. Der Wert der Zeichenkette wird an einem Ort gespeichert, bei Zuweisung/Änderung wird ein Referenzzähler verwendet.

Um die Kompilierungszeit der Plattform zu beschleunigen, haben wir die Stream-Implementierung aus unserer Version von STLPort ausgeschlossen (die wir nicht verwendet haben), was uns eine etwa 20%ige Verkürzung der Kompilierungszeit brachte. Später mussten wir eingeschränkt verwenden In den nächsten zwei Monaten wird die genehmigte Spezifikation in der Phase der Dokumentenvorbereitung zur Veröffentlichung sein, in der an der redaktionellen Bearbeitung von Rechtschreibfehlern und Druckfehlern gearbeitet wird. Anfang November wird die resultierende Version des Dokuments zur Veröffentlichung an die ISO unter dem formalen Namen ISO/IEC 14882:2020 gesendet. In der Zwischenzeit hat der Ausschuss bereits mit der Arbeit am nächsten Standard C++23 (C++2b) begonnen und wird beim nächsten virtuellen Treffen mögliche. Boost nutzt aktiv Streams, insbesondere in seinen Service-APIs (zum Beispiel zum Logging), weshalb wir es modifizieren mussten, indem wir die Nutzung von Streams ausschlossen. Das erschwerte uns den Umstieg auf neue Boost-Versionen.

Der dritte Weg

Bei der Umstellung auf den C++14-Standard haben wir folgende Optionen in Betracht gezogen:

  1. Die von uns modifizierte STLPort auf den C++14-Standard zu portieren. Diese Option ist sehr kompliziert, da die Unterstützung von STLPort 2010 eingestellt wurde und wir den gesamten Code selbst portieren müssten.
  2. Wechsel zu einer anderen STL-Implementierung, die mit C++14 kompatibel ist. Es ist wünschenswert, dass diese Implementierung unter Windows und Linux läuft.
  3. Bei der Kompilierung jede OS einen im entsprechenden Compiler integrierten Bibliothek zu verwenden.

Die erste Option wurde sofort aufgrund des zu großen Arbeitsaufwands abgelehnt.

Wir dachten eine Zeit lang über die zweite Option nach; als Kandidaten betrachteten wir libc++, aber zu diesem Zeitpunkt lief es nicht unter Windows. Um libc++ unter Windows zu portieren, müssten wir viel Arbeit leisten — zum Beispiel alles, was mit Threads, Thread-Synchronisation und Atomicität zu tun hat, selbst zu schreiben, da in libc++ in diesen Bereichen POSIX API.

verwendet wurde, und wir wählten den dritten Weg.

Übergang

Wir mussten also die Nutzung von STLPort durch die Bibliotheken der entsprechenden Compiler ersetzen (Visual Studio 2015 für Windows, gcc 7 für Linux, clang 8 für macOS).

Zum Glück wurde unser Code hauptsächlich nach den Richtlinien geschrieben und verwendete keine ausgeklügelten Tricks, sodass die Migration auf die neuen Bibliotheken vergleichsweise reibungslos verlief, unterstützt durch Skripte, die in den Quelldateien die Namen von Typen, Klassen, Namensräumen und Includes ersetzten. Die Migration betraf 10.000 Quelldateien (von 14.000). wchar_t wurde durch char16_t ersetzt; wir haben uns entschieden, auf die Verwendung von wchar_t zu verzichten, da char16_t auf allen Betriebssystemen 2 Byte einnimmt und die Kompatibilität des Codes zwischen Windows und Linux nicht beeinträchtigt.

Es gab jedoch einige kleine Abenteuer. Zum Beispiel konnte im STLPort-Iterator implizit auf einen Zeiger auf ein Element gecastet werden, was an einigen Stellen in unserem Code verwendet wurde. In den neuen Bibliotheken war das nicht mehr möglich, und diese Stellen mussten manuell analysiert und umgeschrieben werden.

So, die Migration des Codes ist abgeschlossen, der Code wird für alle Betriebssysteme kompiliert. Es ist Zeit für die Tests.

Die Tests nach der Migration zeigten einen Leistungsabfall (stellenweise um 20-30%) und einen Anstieg des Speicherverbrauchs (um 10-15%) im Vergleich zur alten Version des Codes. Dies war unter anderem auf die suboptimale Handhabung von Standardstrings zurückzuführen. Deshalb mussten wir erneut unsere eigene, leicht überarbeitete String-Implementierung verwenden.

Außerdem stellte sich eine interessante Eigenschaft der Container-Implementierungen in den eingebetteten Bibliotheken heraus: Leere (ohne Elemente) std::map und std::set aus den eingebetteten Bibliotheken allozieren Speicher. In unserem Code wurden aufgrund der Implementierungsmerkmale an einigen Stellen viele leere Container dieses Typs erstellt. Die Standardcontainer allozieren wenig Speicher, nur für ein Wurzel-Element, aber für uns war das kritisch – in einigen Szenarien fiel die Leistung spürbar ab und der Speicherverbrauch stieg (im Vergleich zu STLPort). Daher haben wir diese beiden Typen von Containern aus den eingebetteten Bibliotheken in unserem Code durch ihre Implementierung von Boost ersetzt, bei der diese Container nicht diese Besonderheit hatten, und das löste das Problem mit der Verlangsamung und dem erhöhten Speicherverbrauch.

Wie häufig nach umfangreichen Änderungen in großen Projekten, hatte die erste Iteration des Quellcodes einige Probleme, und hier war insbesondere die Unterstützung für Debug-Iteratoren in der Windows-Implementierung sehr hilfreich. Schritt für Schritt kamen wir voran, und bis zum Frühling 2017 (Version 8.3.11 1C:Enterprise) war die Migration abgeschlossen.

Ergebnisse

Der Übergang zu C++14 dauerte etwa 6 Monate. Den Großteil der Zeit arbeitete ein sehr hochqualifizierter Entwickler allein am Projekt, und in der finalen Phase kamen Vertreter der Teams hinzu, die für bestimmte Bereiche zuständig sind — UI, Cluster-Server, Entwicklungs- und Verwaltungstools usw.

Der Übergang hat unsere Arbeit an der Migration zu den neuesten Versionen des Standards erheblich erleichtert. So wurde die Version 1C:Unternehmen 8.3.14 (in Entwicklung, die Veröffentlichung ist für Anfang nächsten Jahres geplant) bereits auf den Standard C++17.

Nach der Migration hatten die Entwickler mehr Möglichkeiten. Früher hatten wir unsere angepasste Version der STL und einen Namensraum std, jetzt befinden sich im Namensraum std die Standardklassen aus den Standardbibliotheken des Compilers, im Namensraum stdx – unsere, für unsere Aufgaben optimierten Strings und Container, und in Boost – die neueste Version von Boost. Der Entwickler verwendet die Klassen, die optimal für die Lösung seiner Aufgaben geeignet sind.

Auch die "native" Implementierung von Move-Konstruktoren (move constructors) für eine Reihe von Klassen hilft bei der Entwicklung. Wenn eine Klasse einen Move-Konstruktor hat und diese Klasse in einen Container eingefügt wird, optimiert die STL das Kopieren der Elemente innerhalb des Containers (zum Beispiel, wenn der Container erweitert wird und die Kapazität verändert und der Speicher neu zugewiesen werden muss).

Ein Wermutstropfen

Das vielleicht unangenehmste (aber nicht kritische) Ergebnis der Migration — wir sind auf ein erhöhtes Volumen gestoßen obj-Dateien, und das vollständige Build-Ergebnis mit allen Zwischenfiles benötigt nun 60 – 70 GB. Dieses Verhalten hängt mit den Eigenschaften moderner Standardbibliotheken zusammen, die weniger kritisch mit dem Volumen der generierten Hilfsdateien umgehen. Dies hat keinen Einfluss auf die Funktion des kompilierten Programms, verursacht jedoch einige Unannehmlichkeiten in der Entwicklung, insbesondere erhöht es die Kompilierungszeit. Auch die Anforderungen an den freien Speicherplatz auf den Build-Servern und den Maschinen der Entwickler steigen. Unsere Entwickler arbeiten parallel an mehreren Versionen der Plattform, und hunderte Gigabyte an Zwischenfiles schaffen manchmal Schwierigkeiten in der Arbeit. Das Problem ist unangenehm, aber nicht kritisch, eine Lösung haben wir vorerst aufgeschoben. Eine der Lösungen, die wir in Betracht ziehen, ist die Technik unity build (sie wird unter anderem von Google bei der Entwicklung des Browsers Chrome verwendet).

Quelle: habr.com

60GB SSD 8Gb DDR4