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

Vor einiger Zeit (Herbst 2016), während der Entwicklung der nächsten Version der Technologieplattform 1C:Enterprise, stellte sich innerhalb des Entwicklungsteams die Frage nach der Unterstützung des neuen Standards C ++ 14 in unserem Code. Der Übergang zu einem neuen Standard würde es uns, wie wir angenommen hatten, ermöglichen, viele Dinge eleganter, einfacher und zuverlässiger zu schreiben und die Unterstützung und Wartung des Codes zu vereinfachen. Und an der Übersetzung scheint nichts Außergewöhnliches zu sein, wenn nicht der Umfang der Codebasis und die spezifischen Merkmale unseres Codes wären.

Für diejenigen, die es nicht wissen: 1C:Enterprise ist eine Umgebung für die schnelle Entwicklung plattformübergreifender Geschäftsanwendungen und Laufzeit für deren Ausführung auf verschiedenen Betriebssystemen und DBMS. Im Allgemeinen enthält das Produkt:

  • Anwendungsserver-Cluster, läuft unter Windows und Linux
  • Auftraggeber, arbeitet mit dem Server über http(s) oder sein eigenes Binärprotokoll, funktioniert unter Windows, Linux, macOS
  • Web-Client, läuft in den Browsern Chrome, Internet Explorer, Microsoft Edge, Firefox, Safari (geschrieben in JavaScript)
  • Entwicklungsumgebung (Konfigurator), funktioniert unter Windows, Linux, macOS
  • Verwaltungstools Anwendungsserver, laufen unter Windows, Linux, macOS
  • Mobiler Client, Verbindung zum Server über http(s), funktioniert auf Mobilgeräten mit Android, iOS, Windows
  • Mobile Plattform – ein Framework zum Erstellen mobiler Offline-Anwendungen mit der Möglichkeit zur Synchronisierung, läuft auf Android, iOS, Windows
  • Entwicklungsumgebung 1C:Enterprise-Entwicklungstools, geschrieben in Java
  • Server Interaktionssysteme

Wir versuchen, so viel wie möglich den gleichen Code für verschiedene Betriebssysteme zu schreiben – die Server-Codebasis ist zu 99 % gleich, die Client-Codebasis zu etwa 95 %. Die 1C:Enterprise-Technologieplattform ist hauptsächlich in C++ geschrieben und die ungefähren Codeeigenschaften sind unten aufgeführt:

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

Und all diese Dinge mussten in C++14 übersetzt werden. Heute erzählen wir Ihnen, wie wir das gemacht haben und was uns dabei begegnet ist.

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

Haftungsausschluss

Alles, was im Folgenden über langsame/schnelle Arbeit und (nicht) großen Speicherverbrauch durch Implementierungen von Standardklassen in verschiedenen Bibliotheken geschrieben wird, bedeutet eines: Dies gilt FÜR UNS. Es ist durchaus möglich, dass Standardimplementierungen für Ihre Aufgaben am besten geeignet sind. Wir begannen mit unseren eigenen Aufgaben: Wir nahmen Daten, die für unsere Kunden typisch waren, führten typische Szenarien darauf durch, schauten uns die Leistung, den verbrauchten Speicher usw. an und analysierten, ob wir und unsere Kunden mit diesen Ergebnissen zufrieden waren oder nicht . Und sie handelten abhängig davon.

Was wir hatten

Zunächst haben wir den Code für die 1C:Enterprise 8-Plattform mit Microsoft Visual Studio geschrieben. Das Projekt begann in den frühen 2000er Jahren und wir hatten eine reine Windows-Version. Natürlich wurde der Code seitdem aktiv weiterentwickelt und viele Mechanismen wurden komplett neu geschrieben. Der Code wurde jedoch gemäß dem Standard von 1998 geschrieben und unsere rechten spitzen Klammern wurden beispielsweise durch Leerzeichen getrennt, damit die Kompilierung erfolgreich war, wie folgt:

vector<vector<int> > IntV;

Im Jahr 2006, mit der Veröffentlichung der Plattformversion 8.1, begannen wir mit der Unterstützung von Linux und wechselten zu einer Standardbibliothek eines Drittanbieters STLPort. Einer der Gründe für den Übergang war das Arbeiten mit breiten Linien. In unserem Code verwenden wir durchgehend std::wstring, das auf dem Typ wchar_t basiert. Seine Größe beträgt unter Windows 2 Byte und unter Linux beträgt die Standardgröße 4 Byte. Dies führte zu einer Inkompatibilität unserer Binärprotokolle zwischen Client und Server sowie verschiedenen persistenten Daten. Mit den gcc-Optionen können Sie festlegen, dass die Größe von wchar_t während der Kompilierung ebenfalls 2 Byte beträgt. Dann können Sie jedoch die Verwendung der Standardbibliothek des Compilers vergessen, weil Es verwendet glibc, das wiederum für ein 4-Byte-wchar_t kompiliert wird. Weitere Gründe waren eine bessere Implementierung von Standardklassen, die Unterstützung von Hash-Tabellen und sogar die Emulation der Semantik der Bewegung innerhalb von Containern, die wir aktiv nutzten. Und ein weiterer Grund war, wie man so schön sagt, die Leistung der Saiten. Wir hatten unsere eigene Klasse für Streicher, weil... Aufgrund der Besonderheiten unserer Software werden String-Operationen sehr häufig verwendet, was für uns von entscheidender Bedeutung ist.

Unsere Saite basiert auf Ideen zur Saitenoptimierung, die bereits Anfang der 2000er Jahre zum Ausdruck kamen Andrei Alexandrescu. Später, als Alexandrescu bei Facebook arbeitete, wurde auf seinen Vorschlag hin eine Zeile in der Facebook-Engine verwendet, die nach ähnlichen Prinzipien funktionierte (siehe Bibliothek). Torheit).

Unsere Linie nutzte zwei Hauptoptimierungstechnologien:

  1. Für kurze Werte wird ein interner Puffer im String-Objekt selbst verwendet (keine zusätzliche Speicherzuweisung erforderlich).
  2. Bei allen anderen kommt Mechanik zum Einsatz Beim Schreiben kopieren. Der String-Wert wird an einem Ort gespeichert und bei der Zuweisung/Änderung wird ein Referenzzähler verwendet.

Um die Plattformkompilierung zu beschleunigen, haben wir die Stream-Implementierung aus unserer STLPort-Variante ausgeschlossen (die wir nicht verwendet haben). Dadurch konnten wir die Kompilierung um etwa 20 % beschleunigen. Anschließend mussten wir nur eingeschränkt davon Gebrauch machen Boost. Boost nutzt Stream stark, insbesondere in seinen Service-APIs (z. B. für die Protokollierung), daher mussten wir es ändern, um die Verwendung von Stream zu entfernen. Dies wiederum machte es für uns schwierig, auf neue Boost-Versionen zu migrieren.

Der dritte Weg

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

  1. Aktualisieren Sie den von uns geänderten STLPort auf den C++14-Standard. Die Option ist sehr schwierig, weil... Die Unterstützung für STLPort wurde 2010 eingestellt und wir mussten den gesamten Code selbst erstellen.
  2. Übergang zu einer anderen STL-Implementierung, die mit C++14 kompatibel ist. Es ist äußerst wünschenswert, dass diese Implementierung für Windows und Linux erfolgt.
  3. Verwenden Sie beim Kompilieren für jedes Betriebssystem die im entsprechenden Compiler integrierte Bibliothek.

Die erste Option wurde aufgrund zu großen Aufwands komplett verworfen.

Wir haben einige Zeit über die zweite Option nachgedacht; als Kandidat betrachtet libc++, aber damals funktionierte es unter Windows nicht. Um libc++ auf Windows zu portieren, müsste man viel Arbeit leisten – zum Beispiel alles selbst schreiben, was mit Threads, Thread-Synchronisation und Atomizität zu tun hat, da libc++ in diesen Bereichen verwendet wird POSIX-API.

Und wir haben den dritten Weg gewählt.

Übergang

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

Glücklicherweise wurde unser Code hauptsächlich nach Richtlinien geschrieben und verzichtete auf allerlei clevere Tricks, so dass die Migration auf neue Bibliotheken relativ reibungslos verlief, mit Hilfe von Skripten, die die Namen von Typen, Klassen, Namespaces und Includes in der Quelle ersetzten Dateien. Die Migration betraf 10 Quelldateien (von 000). wchar_t wurde durch char14_t ersetzt; Wir haben uns entschieden, auf die Verwendung von wchar_t zu verzichten, weil char000_t benötigt auf allen Betriebssystemen 16 Bytes und beeinträchtigt nicht die Codekompatibilität zwischen Windows und Linux.

Es gab einige kleine Abenteuer. Beispielsweise könnte in STLPort ein Iterator implizit in einen Zeiger auf ein Element umgewandelt werden, und an einigen Stellen in unserem Code wurde dies verwendet. In neuen Bibliotheken war dies nicht mehr möglich und diese Passagen mussten manuell analysiert und neu geschrieben werden.

Damit ist die Codemigration abgeschlossen, der Code ist für alle Betriebssysteme kompiliert. Es ist Zeit für Tests.

Tests nach der Umstellung zeigten einen Leistungsabfall (an einigen Stellen bis zu 20–30 %) und einen Anstieg des Speicherverbrauchs (bis zu 10–15 %) im Vergleich zur alten Version des Codes. Dies lag insbesondere an der suboptimalen Performance von Standardsaiten. Daher mussten wir wieder auf unsere eigene, leicht modifizierte Linie zurückgreifen.

Ein interessantes Merkmal der Implementierung von Containern in eingebetteten Bibliotheken wurde ebenfalls enthüllt: Leere (ohne Elemente) std::map und std::set aus integrierten Bibliotheken weisen Speicher zu. Und aufgrund der Implementierungsmerkmale werden an einigen Stellen im Code recht viele leere Container dieses Typs erstellt. Standard-Speichercontainer werden einem Root-Element nur wenig zugewiesen, aber für uns erwies sich dies als kritisch – in einer Reihe von Szenarien sank unsere Leistung erheblich und der Speicherverbrauch stieg (im Vergleich zu STLPort). Daher haben wir in unserem Code diese beiden Arten von Containern aus den integrierten Bibliotheken durch ihre Implementierung von Boost ersetzt, wo diese Container nicht über diese Funktion verfügten, und dadurch das Problem der Verlangsamung und des erhöhten Speicherverbrauchs gelöst.

Wie so oft nach umfangreichen Änderungen in großen Projekten funktionierte die erste Iteration des Quellcodes nicht ohne Probleme, und hier erwies sich insbesondere die Unterstützung für das Debuggen von Iteratoren in der Windows-Implementierung als nützlich. Schritt für Schritt gingen wir voran und im Frühjahr 2017 (Version 8.3.11 1C:Enterprise) war die Migration abgeschlossen.

Ergebnisse

Die Umstellung auf den C++14-Standard hat bei uns etwa 6 Monate gedauert. Meistens arbeitete ein (aber sehr hochqualifizierter) Entwickler an dem Projekt, und in der Endphase schlossen sich Vertreter von Teams an, die für bestimmte Bereiche verantwortlich waren – UI, Servercluster, Entwicklungs- und Verwaltungstools usw.

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

Nach der Migration haben Entwickler mehr Möglichkeiten. Wenn wir früher unsere eigene modifizierte Version von STL und einen std-Namespace hatten, haben wir jetzt Standardklassen aus den integrierten Compiler-Bibliotheken im std-Namespace, im stdx-Namespace – unsere für unsere Aufgaben optimierten Zeilen und Container, in Boost – die neueste Version von Boost. Und der Entwickler nutzt diejenigen Klassen, die optimal zur Lösung seiner Probleme geeignet sind.

Auch die „native“ Implementierung von Move-Konstruktoren hilft bei der Entwicklung (Konstrukteure bewegen) für eine Reihe von Klassen. Wenn eine Klasse über einen Verschiebungskonstruktor verfügt und diese Klasse in einem Container platziert wird, optimiert die STL das Kopieren von Elementen innerhalb des Containers (z. B. wenn der Container erweitert wird und die Kapazität geändert und Speicher neu zugewiesen werden muss).

Haar in der Suppe

Die vielleicht unangenehmste (aber nicht kritischste) Folge der Migration ist, dass wir mit einem Anstieg des Volumens konfrontiert sind obj-Dateien, und das vollständige Ergebnis des Builds mit allen Zwischendateien begann 60–70 GB zu beanspruchen. Dieses Verhalten ist auf die Besonderheiten moderner Standardbibliotheken zurückzuführen, die die Größe der generierten Servicedateien weniger kritisch betrachten. Dies hat keinen Einfluss auf den Betrieb der kompilierten Anwendung, verursacht jedoch eine Reihe von Unannehmlichkeiten bei der Entwicklung, insbesondere erhöht es die Kompilierungszeit. Auch die Anforderungen an freien Speicherplatz auf Build-Servern und Entwicklermaschinen steigen. Unsere Entwickler arbeiten parallel an mehreren Versionen der Plattform, und Hunderte Gigabyte an Zwischendateien erschweren manchmal ihre Arbeit. Das Problem ist unangenehm, aber nicht kritisch; wir haben seine Lösung vorerst verschoben. Wir betrachten Technologie als eine Möglichkeit, dieses Problem zu lösen Einheit aufbauen (insbesondere wird es von Google bei der Entwicklung des Chrome-Browsers verwendet).

Source: habr.com

Kommentar hinzufügen