Jak przetłumaczyliśmy 10 milionów linii kodu C++ na standard C++14 (a następnie na C++17)

Jakiś czas temu (jesienią 2016 roku), podczas prac nad kolejną wersją platformy technologicznej 1C:Enterprise, w zespole deweloperskim pojawiło się pytanie o obsługę nowego standardu C ++ 14 w naszym kodzie. Przejście na nowy standard, jak zakładaliśmy, pozwoliłoby nam pisać wiele rzeczy bardziej elegancko, prosto i niezawodnie oraz uprościłoby obsługę i utrzymanie kodu. I wydaje się, że w tłumaczeniu nie ma nic nadzwyczajnego, gdyby nie skala bazy kodu i specyfika naszego kodu.

Dla tych, którzy nie wiedzą, 1C:Enterprise to środowisko do szybkiego rozwoju wieloplatformowych aplikacji biznesowych i środowiska wykonawczego do ich wykonywania w różnych systemach operacyjnych i DBMS. Ogólnie produkt zawiera:

  • Klaster serwerów aplikacji, działa na systemach Windows i Linux
  • Klient, współpracując z serwerem poprzez http(s) lub własny protokół binarny, działa na systemach Windows, Linux, macOS
  • Klient sieciowy, działający w przeglądarkach Chrome, Internet Explorer, Microsoft Edge, Firefox, Safari (napisany w JavaScript)
  • Środowisko programistyczne (Konfigurator), działa na systemach Windows, Linux, macOS
  • Narzędzia administracyjne serwery aplikacji, działające na systemach Windows, Linux, macOS
  • Klient mobilny, łącząc się z serwerem przez http(s), działa na urządzeniach mobilnych z systemami Android, iOS, Windows
  • Platforma mobilna — framework do tworzenia aplikacji mobilnych offline z możliwością synchronizacji, działający na systemach Android, iOS, Windows
  • Środowisko deweloperskie 1C: Narzędzia rozwoju przedsiębiorstwa, napisany w Javie
  • Server Systemy interakcji

Staramy się w miarę możliwości pisać ten sam kod dla różnych systemów operacyjnych - baza kodu serwera jest w 99% wspólna, baza kodu klienta to około 95%. Platforma technologiczna 1C:Enterprise jest napisana głównie w języku C++, a poniżej podano przybliżoną charakterystykę kodu:

  • 10 milionów linii kodu C++,
  • 14 tys. plików,
  • 60 tysięcy zajęć,
  • pół miliona metod.

I wszystko to musiało zostać przetłumaczone na C++ 14. Dzisiaj opowiemy Wam, jak tego zrobiliśmy i co napotkaliśmy w trakcie tego procesu.

Jak przetłumaczyliśmy 10 milionów linii kodu C++ na standard C++14 (a następnie na C++17)

Zrzeczenie się

Wszystko, co napisano poniżej o powolnej/szybkiej pracy, (nie) dużym zużyciu pamięci przez implementacje standardowych klas w różnych bibliotekach, oznacza jedno: DLA NAS to prawda. Jest całkiem możliwe, że do Twoich zadań najlepiej sprawdzą się standardowe wdrożenia. Zaczęliśmy od własnych zadań: wzięliśmy dane typowe dla naszych klientów, przeprowadziliśmy na nich typowe scenariusze, sprawdziliśmy wydajność, ilość zużywanej pamięci itp. i przeanalizowaliśmy, czy my i nasi klienci jesteśmy zadowoleni z takich wyników, czy nie . I działali w zależności od.

Co mieliśmy

Początkowo kod dla platformy 1C:Enterprise 8 pisaliśmy w Microsoft Visual Studio. Projekt rozpoczął się na początku XXI wieku i mieliśmy wersję tylko dla systemu Windows. Naturalnie, od tego czasu kod był aktywnie rozwijany, wiele mechanizmów zostało całkowicie przepisanych. Ale kod został napisany zgodnie ze standardem z 2000 roku i na przykład nasze nawiasy ostre zostały oddzielone spacjami, aby kompilacja przebiegła pomyślnie, w następujący sposób:

vector<vector<int> > IntV;

W 2006 roku wraz z wydaniem platformy w wersji 8.1 rozpoczęliśmy obsługę systemu Linux i przeszliśmy na standardową bibliotekę innej firmy STLPort. Jednym z powodów przejścia była praca z szerokimi liniami. W naszym kodzie używamy std::wstring, który jest oparty na typie wchar_t. Jego rozmiar w systemie Windows wynosi 2 bajty, a w systemie Linux wartość domyślna to 4 bajty. Doprowadziło to do niezgodności naszych protokołów binarnych między klientem a serwerem, a także do różnych trwałych danych. Korzystając z opcji gcc, możesz określić, że rozmiar wchar_t podczas kompilacji również będzie wynosił 2 bajty, ale wtedy możesz zapomnieć o korzystaniu ze standardowej biblioteki z kompilatora, ponieważ używa glibc, który z kolei jest kompilowany do 4-bajtowego pliku wchar_t. Innymi powodami była lepsza implementacja standardowych klas, obsługa tablic skrótów, a nawet emulacja semantyki poruszania się wewnątrz kontenerów, z czego aktywnie korzystaliśmy. I jeszcze jednym powodem, jak mówią na koniec, była wydajność strun. Mieliśmy własną klasę dla ciągów, ponieważ... Ze względu na specyfikę naszego oprogramowania operacje na ciągach znaków mają bardzo szerokie zastosowanie i jest to dla nas niezwykle istotne.

Nasz ciąg opiera się na pomysłach optymalizacji ciągów wyrażonych na początku XXI wieku Andriej Alexandrescu. Później, gdy Alexandrescu pracował w Facebooku, zgodnie z jego sugestią, w silniku Facebooka zastosowano linię, która działała na podobnych zasadach (patrz biblioteka szaleństwo).

W naszej linii zastosowano dwie główne technologie optymalizacyjne:

  1. W przypadku krótkich wartości używany jest wewnętrzny bufor w samym obiekcie string (nie wymagający dodatkowej alokacji pamięci).
  2. W przypadku wszystkich innych używana jest mechanika Kopiuj przy zapisie. Wartość ciągu jest przechowywana w jednym miejscu, a podczas przypisywania/modyfikacji wykorzystywany jest licznik referencyjny.

Aby przyspieszyć kompilację platformy, wykluczyliśmy implementację strumieniową z naszego wariantu STLPort (z którego nie korzystaliśmy), co dało nam około 20% szybszą kompilację. Następnie musieliśmy korzystać w ograniczonym zakresie Boost. Boost w dużym stopniu wykorzystuje strumień, szczególnie w interfejsach API usług (na przykład do rejestrowania), więc musieliśmy go zmodyfikować, aby wyeliminować użycie strumienia. To z kolei utrudniło nam migrację na nowe wersje Boost.

trzeci sposób

Przechodząc na standard C++14 rozważaliśmy następujące opcje:

  1. Zaktualizuj zmodyfikowany przez nas STLPort do standardu C++ 14. Opcja jest bardzo trudna, bo... wsparcie dla STLPort zostało przerwane w 2010 roku i cały jego kod musieliśmy zbudować sami.
  2. Przejście na inną implementację STL zgodną z C++14. Jest wysoce pożądane, aby ta implementacja była przeznaczona dla systemów Windows i Linux.
  3. Podczas kompilacji dla każdego systemu operacyjnego użyj biblioteki wbudowanej w odpowiedni kompilator.

Opcja pierwsza została odrzucona ze względu na zbyt dużą ilość pracy.

Przez jakiś czas myśleliśmy o drugiej opcji; uważany za kandydata bibliotekac++, ale w tamtym czasie nie działało to pod Windowsem. Aby przenieść bibliotekę libc++ do systemu Windows, trzeba by włożyć dużo pracy - na przykład napisać samodzielnie wszystko, co ma związek z wątkami, synchronizacją wątków i atomowością, ponieważ biblioteka libc++ jest używana w tych obszarach API POSIX-a.

I wybraliśmy trzecią drogę.

Przejście

Musieliśmy więc zastąpić użycie STLPort bibliotekami odpowiednich kompilatorów (Visual Studio 2015 dla Windows, gcc 7 dla Linuksa, clang 8 dla macOS).

Na szczęście nasz kod był pisany głównie według wytycznych i nie korzystał z wszelkiego rodzaju sprytnych chwytów, więc migracja do nowych bibliotek przebiegła stosunkowo sprawnie, przy pomocy skryptów, które zastępowały nazwy typów, klas, przestrzeni nazw i zawiera w źródle akta. Migracja dotyczyła 10 000 plików źródłowych (z 14 000). wchar_t został zastąpiony przez char16_t; zdecydowaliśmy się porzucić użycie wchar_t, ponieważ char16_t zajmuje 2 bajty we wszystkich systemach operacyjnych i nie psuje kompatybilności kodu między Windows i Linux.

Było kilka małych przygód. Na przykład w STLPort iterator można niejawnie rzutować na wskaźnik do elementu i w niektórych miejscach naszego kodu zostało to wykorzystane. W nowych bibliotekach nie było już takiej możliwości i fragmenty te trzeba było analizować i przepisywać ręcznie.

Tak więc migracja kodu została zakończona, kod jest kompilowany dla wszystkich systemów operacyjnych. Czas na testy.

Testy po przejściu wykazały spadek wydajności (w niektórych miejscach nawet o 20-30%) i wzrost zużycia pamięci (nawet o 10-15%) w porównaniu do starej wersji kodu. Było to w szczególności spowodowane nieoptymalną wydajnością standardowych ciągów. Dlatego znów musieliśmy skorzystać z własnej, nieco zmodyfikowanej linii.

Ujawniono także ciekawą cechę implementacji kontenerów w bibliotekach wbudowanych: puste (bez elementów) std::map i std::set z bibliotek wbudowanych alokują pamięć. A ze względu na funkcje implementacyjne, w niektórych miejscach kodu powstaje całkiem sporo pustych kontenerów tego typu. Standardowe kontenery pamięci są przydzielane trochę, na jeden element główny, ale dla nas okazało się to krytyczne - w wielu scenariuszach nasza wydajność znacznie spadła, a zużycie pamięci wzrosło (w porównaniu do STLPort). Dlatego w naszym kodzie zastąpiliśmy te dwa typy kontenerów z bibliotek wbudowanych ich implementacją z Boost, gdzie kontenery te nie posiadały tej funkcji, co rozwiązało problem spowolnienia i zwiększonego zużycia pamięci.

Jak to często bywa po zmianach na dużą skalę w dużych projektach, pierwsza iteracja kodu źródłowego nie przebiegła bez problemów i tutaj szczególnie przydała się obsługa debugowania iteratorów w implementacji Windows. Krok po kroku posuwaliśmy się do przodu i wiosną 2017 (wersja 8.3.11 1C:Enterprise) migracja została zakończona.

Wyniki

Przejście na standard C++14 zajęło nam około 6 miesięcy. Przez większość czasu nad projektem pracował jeden (ale bardzo wykwalifikowany) programista, a na końcowym etapie dołączyli przedstawiciele zespołów odpowiedzialnych za konkretne obszary – interfejs użytkownika, klaster serwerów, narzędzia programistyczne i administracyjne itp.

Przejście znacznie ułatwiło nam pracę nad migracją do najnowszych wersji standardu. Tym samym wersja 1C:Enterprise 8.3.14 (w fazie rozwoju, wydanie zaplanowane na początek przyszłego roku) została już przeniesiona do standardu C++17.

Po migracji programiści mają więcej opcji. Jeśli wcześniej mieliśmy własną zmodyfikowaną wersję STL i jedną przestrzeń nazw std, teraz mamy standardowe klasy z wbudowanych bibliotek kompilatorów w przestrzeni nazw std, w przestrzeni nazw stdx - nasze linie i kontenery zoptymalizowane pod nasze zadania, w boost - najnowsza wersja boosta. A programista używa tych klas, które są optymalnie dostosowane do rozwiązania jego problemów.

„Natywna” implementacja konstruktorów przenoszenia również pomaga w rozwoju (przenieść konstruktory) dla kilku klas. Jeśli klasa ma konstruktor przenoszenia i ta klasa jest umieszczona w kontenerze, wówczas STL optymalizuje kopiowanie elementów wewnątrz kontenera (na przykład, gdy kontener jest rozwinięty i konieczna jest zmiana pojemności i ponowna alokacja pamięci).

Leć w maści

Być może najbardziej nieprzyjemną (ale nie krytyczną) konsekwencją migracji jest to, że mamy do czynienia ze wzrostem jej wolumenu pliki obj, a pełny wynik kompilacji ze wszystkimi plikami pośrednimi zaczął zajmować 60–70 GB. Takie zachowanie wynika ze specyfiki nowoczesnych bibliotek standardowych, które stały się mniej krytyczne w stosunku do rozmiaru generowanych plików usług. Nie ma to wpływu na działanie skompilowanej aplikacji, jednak powoduje szereg niedogodności w rozwoju, w szczególności wydłuża czas kompilacji. Rosną także wymagania dotyczące wolnego miejsca na serwerach kompilacji i maszynach programistycznych. Nasi programiści pracują równolegle na kilku wersjach platformy, a setki gigabajtów plików pośrednich czasami stwarzają trudności w ich pracy. Problem jest nieprzyjemny, ale nie krytyczny, na razie odłożyliśmy jego rozwiązanie. Rozważamy technologię jako jedną z opcji rozwiązania tego problemu budowanie jedności (w szczególności Google wykorzystuje je przy opracowywaniu przeglądarki Chrome).

Źródło: www.habr.com

Dodaj komentarz