Vor einiger Zeit (im Herbst 2016) stellte sich im Entwicklungsteam bei der Entwicklung einer neuen Version der technologischen Plattform 1C:Enterprise die Frage nach der Unterstützung des neuen Standards in unserem Code. Der Wechsel zu diesem neuen Standard würde es uns, wie wir annahmen, ermöglichen, viele Dinge eleganter, einfacher und zuverlässiger zu gestalten, und die Wartung und Pflege des Codes zu vereinfachen. Und auf den ersten Blick scheint die Übersetzung nicht außergewöhnlich zu sein, wenn man jedoch die Größe der Codebasis und die spezifischen Merkmale unseres Codes berücksichtigt.
Für diejenigen, die es nicht wissen: 1C:Enterprise ist eine Umgebung für die schnelle Entwicklung plattformübergreifender Geschäftsanwendungen und ein Runtime-System für deren Ausführung in verschiedenen Betriebssystemen und Datenbanksystemen. Im Großen und Ganzen umfasst das Produkt:
- , arbeitet unter Windows und Linux
- , kommuniziert mit dem Server über http(s) oder über ein eigenes binäres Protokoll, läuft unter Windows, Linux, macOS
- , läuft in den Browsern Chrome, Internet Explorer, Microsoft Edge, Firefox, Safari (geschrieben in JavaScript)
- Entwicklungsumgebung (), läuft unter Windows, Linux, macOS
- für Anwendungsserver, laufen unter Windows, Linux, macOS
- , der sich über http(s) mit dem Server verbindet, funktioniert auf mobilen Geräten mit Android, iOS und Windows.
- — ein Framework zur Erstellung von Offline-Mobilanwendungen mit Synchronisierungsfunktion, die auf Android, iOS und Windows laufen.
- Entwicklungsumgebung , geschrieben in Java
- Server
Wir bemühen uns, möglichst einen Code für verschiedene Betriebssysteme zu schreiben — die Codebasis des Servers ist zu 99 % gemeinsam, die des Clients etwa zu 95 %. Die technologische Plattform 1C:Enterprise 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 portiert werden. Wie wir das gemacht haben und auf was wir dabei gestoßen sind, werden wir heute erzählen.

Haftungsausschluss
Alles, was unten über die langsame/schnelle Leistung und den (nicht) hohen Speicherverbrauch von Standardklassen in verschiedenen Bibliotheken geschrieben steht, bedeutet Folgendes: Das gilt FÜR UNS. Möglicherweise sind die Standardimplementierungen für Ihre Anforderungen die besten Lösungen. Wir haben uns jedoch an unseren Aufgaben orientiert: Wir haben typische Daten unserer Kunden genommen, typische Szenarien durchlaufen und die Leistung, den Speicherverbrauch usw. analysiert, um festzustellen, ob die Ergebnisse uns und unseren Kunden zufriedenstellen oder nicht. Und wir haben je nach Bedarf gehandelt.
Was wir hatten
Ursprünglich haben wir den Code für die Plattform 1C:Enterprise 8 in Microsoft Visual Studio geschrieben. Das Projekt begann Anfang der 2000er Jahre und wir hatten nur eine Version für Windows. Natürlich hat sich seitdem der Code aktiv weiterentwickelt, viele Mechanismen wurden vollständig neu geschrieben. Der Code wurde jedoch nach dem Standard von 1998 geschrieben, und beispielsweise waren die geschlossenen rechten Klammern durch Leerzeichen getrennt, damit die Kompilierung erfolgreich war, 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 . Einer der Gründe für den Wechsel war die Arbeit mit breiten Zeichenfolgen. In unserem Code verwenden wir überall std::wstring, das auf dem wchar_t-Typ basiert. Seine Größe beträgt in Windows 2 Byte und in Linux standardmäßig 4 Byte. Dies führte zu Inkompatibilitäten 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 während der Kompilierung ebenfalls 2 Byte betragen soll, aber dann kann man die Verwendung der Standardbibliothek des Compilers vergessen, da sie glibc verwendet, die wiederum für 4-Byte-wchar_t kompiliert ist. Weitere Gründe waren die qualitativ hochwertigere Implementierung der Standardklassen, die Unterstützung von Hash-Tabellen und sogar die Emulation der Semantik von Bewegungen innerhalb von Containern, die wir aktiv verwendet haben. Und ein weiterer Grund, wie man sagt, last but not least, war die Leistungsfähigkeit der Strings. Wir hatten unsere eigene Klasse für Strings, da in unserer Software-Spezifikationen stringoperationen sehr weit verbreitet sind und für uns entscheidend sind.
Unser String basiert auf den Ideen der Stringoptimierung, die bereits zu Beginn der 2000er Jahre geäußert wurden. . Später, als Alexandrescu bei Facebook arbeitete, wurde auf seine Anregung hin eine ähnliche Zeile in der Facebook-Engine implementiert (siehe Bibliothek ).
. In unserer Zeile verwendeten wir zwei Hauptoptimierungstechnologien:
- Für kurze Werte nutzen wir einen internen Puffer im Objekt selbst (der keine zusätzliche Speicherallokation benötigt).
- Für alle anderen kommt die Mechanik . Der Wert der Zeichenkette wird an einem Ort gespeichert; bei Zuweisung/Änderung kommt ein Referenzzähler zum Einsatz.
Um die Kompilierung der Plattform zu beschleunigen, haben wir die Stream-Implementierung aus unserer STLPort-Version ausgeschlossen (die wir nicht verwendet haben), was uns eine Kompilierungsbeschleunigung von etwa 20 % einbrachte. Anschließend mussten wir Boost Boost nutzt aktiv Streams, insbesondere in seinen Service-APIs (z. B. für Logging), weshalb wir es modifizieren mussten, um die Verwendung von Streams auszuschließen. Das erschwerte uns wiederum den Übergang zu neuen Boost-Versionen.
Der dritte Weg
Beim Übergang auf den C++14-Standard haben wir folgende Optionen in Betracht gezogen:
- Die Anpassung des von uns modifizierten STLPort an den C++14 Standard. Diese Option ist sehr komplex, da die Unterstützung für STLPort 2010 eingestellt wurde und wir den gesamten Code selbst anpassen müssten.
- Der Umstieg auf eine andere STL-Implementierung, die mit C++14 kompatibel ist. Es wäre äußerst wünschenswert, wenn diese Implementierung unter Windows und Linux verfügbar wäre.
- Bei der Kompilierung jede OS-spezifische Bibliothek des entsprechenden Compilers zu verwenden.
Die erste Option wurde sofort verworfen wegen des zu hohen Arbeitsaufwands.
Wir haben eine Weile über die zweite Option nachgedacht; dabei zogen wir in Betracht, , aber diese funktionierte zu dem Zeitpunkt nicht unter Windows. Um libc++ nach Windows zu portieren, hätte man eine Menge Arbeit leisten müssen – beispielsweise alles, was mit Threads, Thread-Synchronisation und Atomizität zu tun hat, selbst zu implementieren, da in libc++ in diesen Bereichen das .
verwendet wurde.
Und wir wählten den dritten Weg.
Übergang. Daher mussten wir die Verwendung 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 Richtlinien entwickelt und es wurden keine ungewöhnlichen Tricks verwendet, sodass die Migration auf neue Bibliotheken vergleichsweise reibungslos verlief. Dies geschah mit Hilfe von Skripten, die in den Quelldateien die Namen von Typen, Klassen, Namespaces und Includes ersetzten. Die Migration betraf 10.000 Quelldateien (von 14.000). wchar_t wurde durch char16_t ersetzt; wir haben uns entschieden, auf wchar_t zu verzichten, da char16_t auf allen Betriebssystemen 2 Bytes belegt und die Kompatibilität des Codes zwischen Windows und Linux nicht beeinträchtigt.
Ohne kleine Abenteuer ging es nicht. Zum Beispiel konnte man in STLPort einen Iterator implizit in einen Zeiger auf ein Element casten, was an einigen Stellen unseres Codes verwendet wurde. In den neuen Bibliotheken war dies nicht mehr möglich, und diese Stellen mussten manuell analysiert und umgeschrieben werden.
Also, die Code-Migration ist abgeschlossen, der Code wird für alle Betriebsysteme kompiliert. Es ist Zeit für die Tests.
Die Tests nach dem Wechsel zeigten einen Rückgang der Leistung (stellenweise bis zu 20-30%) und einen Anstieg des Speicherverbrauchs (bis zu 10-15%) im Vergleich zur alten Codeversion. Dies war unter anderem auf die suboptimale Handhabung der Standard-Strings zurückzuführen. Daher mussten wir erneut unsere eigene, leicht modifizierte String-Implementierung verwenden.
Es stellte sich auch eine interessante Besonderheit bei der Implementierung von Containern in den eingebetteten Bibliotheken heraus: Leere (ohne Elemente) std::map und std::set aus den eingebetteten Bibliotheken allokieren Speicher. In unserem Code werden aufgrund spezifischer Implementierungsdetails an einigen Stellen ziemlich viele leere Container dieses Typs erstellt. Die Standardcontainer allokieren zwar nur wenig Speicher, für ein einzelnes Wurzel-Element, aber für uns war das kritisch – in mehreren Szenarien fiel die Leistung spürbar ab und der Speicherverbrauch stieg (im Vergleich zu STLPort). Deshalb haben wir in unserem Code diese beiden Containertypen aus den eingebetteten Bibliotheken durch deren Implementierung von Boost ersetzt, wo diese Container nicht die gleiche Problematik aufwiesen, und das hat das Problem mit der Verlangsamung sowie dem erhöhten Speicherverbrauch gelöst.
Wie so oft nach umfangreichen Änderungen in großen Projekten, hatte die erste Iteration des Quellcodes einige Probleme, und hier kam uns besonders die Unterstützung durch Debugging-Iteratoren in der Windows-Implementierung zugute. Schritt für Schritt arbeiteten wir uns voran, und bis zum Frühjahr 2017 (Version 8.3.11 1C:Enterprise) war die Migration abgeschlossen.
Ergebnisse
Der Wechsel zu C++14 dauerte bei uns etwa 6 Monate. Die meiste Zeit arbeitete ein einzelner (aber sehr qualifizierter) Entwickler am Projekt, und in der finalen Phase schlossen sich Vertreter der Teams an, die für bestimmte Bereiche verantwortlich waren — UI, Cluster-Server, Entwicklungs- und Verwaltungstools usw.
Der Übergang hat unsere Arbeit an der Migration zu den neuesten Versionen des Standards erheblich vereinfacht. So wurde die Version 1C:Enterprise 8.3.14 (in Entwicklung, die Veröffentlichung ist für Anfang nächsten Jahres geplant) bereits auf den Standard umgestellt. .
Nach der Migration haben die Entwickler mehr Möglichkeiten. Während wir früher eine angepasste Version von STL und einen Namensraum std hatten, befinden sich jetzt die Standardklassen aus den eingebauten Bibliotheken des Compilers im Namensraum std, unsere für unsere Bedürfnisse optimierten Strings und Container im Namensraum stdx und die neueste Version von Boost im Namensraum Boost. Der Entwickler kann die Klassen verwenden, die am besten für seine Aufgaben geeignet sind.
Auch die „native“ Implementierung der Move-Konstruktoren hilft bei der Entwicklung () für eine Reihe von Klassen. Wenn eine Klasse einen Move-Konstruktor hat und diese Klasse in einen Container eingefügt wird, optimiert STL die Kopieroperationen innerhalb des Containers (zum Beispiel, wenn der Container erweitert wird und die Kapazität geändert sowie der Speicher neu zugewiesen werden muss).
Ein negativer Punkt
Das vielleicht unangenehmste (aber nicht kritische) Ergebnis der Migration ist, dass wir mit einer Zunahme von , und das endgültige Build-Ergebnis mit allen Zwischendateien benötigt 60 bis 70 GB. Dieses Verhalten hängt mit den Eigenschaften moderner Standardbibliotheken zusammen, die weniger kritisch zum Umfang der generierten Hilfsdateien stehen. Es beeinflusst nicht die Funktion der kompilierten Anwendung, bringt jedoch einige Unannehmlichkeiten in der Entwicklung mit sich, insbesondere verlängert es die Kompilierungszeit. Auch die Anforderungen an den verfügbaren Speicherplatz auf den Build-Servern und den Maschinen der Entwickler steigen. Unsere Entwickler arbeiten parallel an mehreren Versionen der Plattform, und Hunderte von Gigabyte an Zwischendateien schaffen manchmal Schwierigkeiten bei der Arbeit. Das Problem ist unangenehm, aber nicht kritisch; wir haben die Lösung vorerst aufgeschoben. Eine der in Betracht gezogenen Lösungsansätze ist die Technik (diese wird unter anderem von Google bei der Entwicklung des Browsers Chrome verwendet).
Quelle: habr.com
