C++ Russland: wie es passiert ist

Wenn Sie zu Beginn des Stücks sagen, dass C++-Code an der Wand hängt, wird es Ihnen am Ende bestimmt in den Fuß schießen.

Bjarne Stroustrup

Vom 31. Oktober bis 1. November fand in St. Petersburg die C++ Russia Piter-Konferenz statt – eine der größten Programmierkonferenzen in Russland, organisiert von der JUG Ru Group. Zu den eingeladenen Rednern gehören Mitglieder des C++ Standards Committee, CppCon-Redner, O'Reilly-Buchautoren und Betreuer von Projekten wie LLVM, libc++ und Boost. Die Konferenz richtet sich an erfahrene C++-Entwickler, die ihr Fachwissen vertiefen und Erfahrungen in der Live-Kommunikation austauschen möchten. Studenten, Doktoranden und Hochschullehrer erhalten sehr attraktive Rabatte.

Die Moskauer Ausgabe der Konferenz wird bereits im April nächsten Jahres zu besichtigen sein, aber in der Zwischenzeit werden Ihnen unsere Studenten erzählen, welche interessanten Dinge sie bei der letzten Veranstaltung gelernt haben. 

C++ Russland: wie es passiert ist

Fotos von Konferenzalbum

Über uns

Zwei Studenten der National Research University Higher School of Economics – St. Petersburg haben an diesem Beitrag mitgearbeitet:

  • Liza Vasilenko studiert im vierten Studienjahr Programmiersprachen im Rahmen des Studiengangs Angewandte Mathematik und Informatik. Nachdem ich die Sprache C++ bereits in meinem ersten Studienjahr kennengelernt hatte, sammelte ich anschließend durch Praktika in der Industrie Erfahrungen im Umgang damit. Meine Leidenschaft für Programmiersprachen im Allgemeinen und funktionale Programmierung im Besonderen hat die Auswahl der Berichte auf der Konferenz geprägt.
  • Danya Smirnov ist Studentin im ersten Jahr des Masterstudiengangs „Programmierung und Datenanalyse“. Noch während meiner Schulzeit habe ich Olympiade-Aufgaben in C++ geschrieben, und dann kam es irgendwie vor, dass die Sprache immer wieder in Bildungsaktivitäten auftauchte und schließlich zur Hauptarbeitssprache wurde. Ich habe mich entschieden, an der Konferenz teilzunehmen, um mein Wissen zu verbessern und neue Möglichkeiten kennenzulernen.

Im Newsletter informiert die Fakultätsleitung häufig über Bildungsveranstaltungen rund um unser Fachgebiet. Im September sahen wir Informationen über C++ Russland und beschlossen, uns als Zuhörer zu registrieren. Dies ist unsere erste Erfahrung mit der Teilnahme an solchen Konferenzen.

Konferenzstruktur

  • Доклады

Innerhalb von zwei Tagen lasen Experten 30 Berichte zu vielen aktuellen Themen: geniale Nutzung von Sprachfunktionen zur Lösung angewandter Probleme, bevorstehende Sprachaktualisierungen im Zusammenhang mit dem neuen Standard, Kompromisse im C++-Design und Vorsichtsmaßnahmen bei der Arbeit mit deren Konsequenzen, Beispiele einer interessanten Projektarchitektur sowie einige Details der Sprachinfrastruktur unter der Haube. Drei Aufführungen fanden gleichzeitig statt, meist zwei auf Russisch und eine auf Englisch.

  • Diskussionszonen

Nach der Rede wurden alle offenen Fragen und nicht abgeschlossenen Diskussionen in speziell dafür vorgesehene Bereiche zur Kommunikation mit den Rednern verlegt, die mit Markierungstafeln ausgestattet waren. Eine gute Möglichkeit, die Pause zwischen den Reden mit einem angenehmen Gespräch zu überbrücken.

  • Lightning Talks und informelle Diskussionen

Wenn Sie einen kurzen Bericht halten möchten, können Sie sich am Whiteboard für den abendlichen Lightning Talk anmelden und erhalten fünf Minuten Zeit, um über alles zum Konferenzthema zu sprechen. Zum Beispiel eine kurze Einführung in Sanitizer für C++ (für einige war es neu) oder eine Geschichte über einen Fehler in der Sinuswellenerzeugung, der nur zu hören, aber nicht zu sehen ist.

Ein weiteres Format ist die Podiumsdiskussion „With a Heart to Heart Committee“. Auf der Bühne stehen einige Mitglieder des Standardisierungsausschusses, auf dem Projektor steht ein Kamin (offiziell – um eine aufrichtige Atmosphäre zu schaffen, aber der Grund „weil ALLES IN FEUER IST“ scheint lustiger), Fragen zum Standard und zur allgemeinen Vision von C++ , ohne hitzige Fachdiskussionen und Feiertage. Es stellte sich heraus, dass dem Komitee auch lebende Menschen angehören, die sich möglicherweise nicht ganz sicher sind oder etwas nicht wissen.

Für Holivar-Fans blieb die dritte Veranstaltung zum Thema – die BOF-Sitzung „Go vs. C++“. Wir nehmen einen Go-Liebhaber, einen C++-Liebhaber, vor Beginn der Sitzung bereiten sie gemeinsam 100500 Folien zu einem Thema vor (z. B. Probleme mit Paketen in C++ oder das Fehlen von Generika in Go), und dann führen sie eine lebhafte Diskussion untereinander und mit dem Publikum, und das Publikum versucht, zwei Standpunkte gleichzeitig zu verstehen. Wenn ein Holivar außerhalb des Kontexts beginnt, greift der Moderator ein und versöhnt die Parteien. Dieses Format macht süchtig: Mehrere Stunden nach dem Start war erst die Hälfte der Folien fertig. Das Ende musste stark beschleunigt werden.

  • Partner steht

Die Partner der Konferenz waren in den Hallen vertreten – an den Ständen sprachen sie über aktuelle Projekte, boten Praktika und Anstellungen an, veranstalteten Quizze und kleine Wettbewerbe und verlosten auch schöne Preise. Gleichzeitig boten einige Unternehmen sogar an, die ersten Interviews zu durchlaufen, was für diejenigen, die kamen, nicht nur zum Anhören von Berichten nützlich sein könnte.

Technische Details der Berichte

An beiden Tagen haben wir uns Berichte angehört. Manchmal war es schwierig, aus den Parallelberichten einen auszuwählen – wir einigten uns darauf, uns aufzuteilen und in den Pausen die gewonnenen Erkenntnisse auszutauschen. Und dennoch scheint vieles ausgelassen zu werden. Hier möchten wir über den Inhalt einiger der Berichte sprechen, die wir am interessantesten fanden

Ausnahmen in C++ durch das Prisma der Compiler-Optimierungen, Roman Rusyaev

C++ Russland: wie es passiert ist
Schieben Sie von Präsentationen

Wie der Titel vermuten lässt, untersuchte Roman die Arbeit mit Ausnahmen am Beispiel von LLVM. Gleichzeitig kann der Bericht denjenigen, die Clang bei ihrer Arbeit nicht verwenden, dennoch eine Vorstellung davon geben, wie der Code möglicherweise optimiert werden könnte. Dies liegt daran, dass die Entwickler von Compilern und entsprechenden Standardbibliotheken miteinander kommunizieren und viele erfolgreiche Lösungen zusammenfallen können.

Um eine Ausnahme zu behandeln, müssen Sie also viele Dinge tun: den Behandlungscode aufrufen (falls vorhanden) oder Ressourcen auf der aktuellen Ebene freigeben und den Stapel höher hochfahren. All dies führt dazu, dass der Compiler zusätzliche Anweisungen für Aufrufe hinzufügt, die möglicherweise Ausnahmen auslösen. Wenn die Ausnahme also nicht tatsächlich ausgelöst wird, führt das Programm dennoch unnötige Aktionen aus. Um den Overhead irgendwie zu reduzieren, verfügt LLVM über mehrere Heuristiken zur Bestimmung von Situationen, in denen kein Ausnahmebehandlungscode hinzugefügt werden muss oder die Anzahl „zusätzlicher“ Anweisungen reduziert werden kann.

Der Referent untersucht etwa ein Dutzend davon und zeigt sowohl Situationen auf, in denen sie zur Beschleunigung der Programmausführung beitragen, als auch solche, in denen diese Methoden nicht anwendbar sind.

Daher führt Roman Rusyaev die Schüler zu dem Schluss, dass Code, der die Ausnahmebehandlung enthält, nicht immer ohne Overhead ausgeführt werden kann, und gibt den folgenden Rat:

  • Bei der Entwicklung von Bibliotheken lohnt es sich, grundsätzlich auf Ausnahmen zu verzichten.
  • Wenn weiterhin Ausnahmen erforderlich sind, lohnt es sich, wann immer möglich, überall die Modifikatoren noexclusive (und const) hinzuzufügen, damit der Compiler so viel wie möglich optimieren kann.

Generell bestätigte der Referent die Auffassung, dass Ausnahmen am besten auf ein Minimum ausgeschöpft oder ganz aufgegeben werden sollten.

Die Berichtsfolien sind unter folgendem Link verfügbar: [„C++-Ausnahmen aus der Sicht der LLVM-Compiler-Optimierungen“]

Generatoren, Coroutinen und andere hirnentfaltende Süße, Adi Shavit

C++ Russland: wie es passiert ist
Schieben Sie von Präsentationen

Einer der vielen Berichte auf dieser Konferenz, die sich mit Innovationen in C++20 befassten, beeindruckte nicht nur durch seine farbenfrohe Präsentation, sondern auch durch die klare Identifizierung bestehender Probleme mit der Sammlungsverarbeitungslogik (for-Schleife, Rückrufe).

Adi Shavit hebt Folgendes hervor: Die derzeit verfügbaren Methoden durchlaufen die gesamte Sammlung und bieten keinen Zugriff auf einen internen Zwischenstatus (oder sie tun dies im Fall von Rückrufen, aber mit vielen unangenehmen Nebenwirkungen, wie z. B. der Callback-Hölle). . Es scheint, dass es Iteratoren gibt, aber selbst mit ihnen läuft nicht alles so reibungslos: Es gibt keine gemeinsamen Ein- und Ausstiegspunkte (begin → end versus rbegin → rend usw.), es ist nicht klar, wie lange wir iterieren werden? Ab C++20 sind diese Probleme gelöst!

Erste Option: Bereiche. Durch das Einschließen von Iteratoren erhalten wir eine gemeinsame Schnittstelle für den Anfang und das Ende einer Iteration und erhalten außerdem die Möglichkeit zum Verfassen. All dies erleichtert den Aufbau vollwertiger Datenverarbeitungspipelines. Aber nicht alles läuft so reibungslos: Ein Teil der Berechnungslogik befindet sich in der Implementierung eines bestimmten Iterators, was das Verstehen und Debuggen des Codes erschweren kann.

C++ Russland: wie es passiert ist
Schieben Sie von Präsentationen

Nun, für diesen Fall hat C++20 Coroutinen hinzugefügt (Funktionen, deren Verhalten den Generatoren in Python ähnelt): Die Ausführung kann verzögert werden, indem ein aktueller Wert zurückgegeben wird, während ein Zwischenzustand beibehalten wird. Auf diese Weise können wir nicht nur mit den Daten arbeiten, wie sie erscheinen, sondern auch die gesamte Logik in einer bestimmten Coroutine kapseln.

Aber es gibt einen Wermutstropfen: Derzeit werden sie von bestehenden Compilern nur teilweise unterstützt und sind auch nicht so sauber implementiert, wie wir es gerne hätten: Beispielsweise lohnt es sich noch nicht, Referenzen und temporäre Objekte in Coroutinen zu verwenden. Darüber hinaus gibt es einige Einschränkungen hinsichtlich der möglichen Coroutinen, und constexpr-Funktionen, Konstruktoren/Destruktoren und main sind in dieser Liste nicht enthalten.

Somit lösen Coroutinen einen erheblichen Teil der Probleme durch die Einfachheit der Datenverarbeitungslogik, ihre aktuellen Implementierungen müssen jedoch verbessert werden.

Material:

C++-Tricks von Yandex.Taxi, Anton Polukhin

In meiner beruflichen Tätigkeit muss ich manchmal reine Hilfsdinge implementieren: einen Wrapper zwischen der internen Schnittstelle und der API einer Bibliothek, Protokollierung oder Analyse. In diesem Fall ist in der Regel keine zusätzliche Optimierung erforderlich. Was aber, wenn diese Komponenten in einigen der beliebtesten Dienste im RuNet verwendet werden? In einer solchen Situation müssen Sie allein Protokolle im Terabyte-Bereich pro Stunde verarbeiten! Dann zählt jede Millisekunde und deshalb muss man auf verschiedene Tricks zurückgreifen – Anton Polukhin hat darüber gesprochen.

Das vielleicht interessanteste Beispiel war die Implementierung des Pointer-to-Implementation-Musters (Pimpl). 

#include <third_party/json.hpp> //PROBLEMS! 
struct Value { 
    Value() = default; 
    Value(Value&& other) = default; 
    Value& operator=(Value&& other) = default; 
    ~Value() = default; 

    std::size_t Size() const { return data_.size(); } 

private: 
    third_party::Json data_; 
};

In diesem Beispiel möchte ich zunächst die Header-Dateien externer Bibliotheken entfernen. Dadurch wird die Kompilierung beschleunigt und Sie können sich vor möglichen Namenskonflikten und anderen ähnlichen Fehlern schützen. 

Okay, wir haben #include in die .cpp-Datei verschoben: Wir benötigen eine Forward-Deklaration der umschlossenen API sowie std::unique_ptr. Jetzt haben wir dynamische Zuweisungen und andere unangenehme Dinge wie Daten, die über eine Menge Daten verstreut sind, und reduzierte Garantien. std::aligned_storage kann dabei helfen. 

struct Value { 
// ... 
private: 
    using JsonNative = third_party::Json; 
    const JsonNative* Ptr() const noexcept; 
    JsonNative* Ptr() noexcept; 

    constexpr std::size_t kImplSize = 32; 
    constexpr std::size_t kImplAlign = 8; 
    std::aligned_storage_t<kImplSize, kImplAlign> data_; 
};

Das einzige Problem: Sie müssen die Größe und Ausrichtung für jeden Wrapper angeben – erstellen wir unsere Pimpl-Vorlage mit Parametern , verwenden Sie einige beliebige Werte und überprüfen Sie den Destruktor, ob wir alles richtig gemacht haben: 

~FastPimpl() noexcept { 
    validate<sizeof(T), alignof(T)>(); 
    Ptr()->~T(); 
}

template <std::size_t ActualSize, std::size_t ActualAlignment>
static void validate() noexcept { 
    static_assert(
        Size == ActualSize, 
        "Size and sizeof(T) mismatch"
    ); 
    static_assert(
        Alignment == ActualAlignment, 
        "Alignment and alignof(T) mismatch"
    ); 
}

Da T bei der Verarbeitung des Destruktors bereits definiert ist, wird dieser Code korrekt analysiert und in der Kompilierungsphase werden die erforderlichen Größen- und Ausrichtungswerte ausgegeben, die als Fehler eingegeben werden müssen. Auf diese Weise entledigen wir uns auf Kosten eines zusätzlichen Kompilierungslaufs der dynamischen Zuordnung verpackter Klassen, verstecken die API in einer .cpp-Datei mit der Implementierung und erhalten außerdem ein Design, das sich besser für die Zwischenspeicherung durch den Prozessor eignet.

Protokollierung und Analyse schienen weniger beeindruckend und werden daher in diesem Testbericht nicht erwähnt.

Die Berichtsfolien sind unter folgendem Link verfügbar: ["C++-Tricks von Taxi"]

Moderne Techniken, um Ihren Code trocken zu halten, Björn Fahller

In diesem Vortrag zeigt Björn Fahller verschiedene Möglichkeiten auf, dem Stilfehler wiederholter Zustandsprüfungen entgegenzuwirken:

assert(a == IDLE || a == CONNECTED || a == DISCONNECTED);

Klingt bekannt? Durch die Verwendung mehrerer leistungsstarker C++-Techniken, die in neueren Standards eingeführt wurden, können Sie dieselbe Funktionalität elegant und ohne Leistungseinbußen implementieren. Vergleichen:   

assert(a == any_of(IDLE, CONNECTED, DISCONNECTED));

Um eine nicht festgelegte Anzahl von Prüfungen zu bewältigen, müssen Sie sofort variadische Vorlagen und Faltausdrücke verwenden. Nehmen wir an, wir wollen die Gleichheit mehrerer Variablen mit dem state_type-Element der Aufzählung prüfen. Das erste, was mir in den Sinn kommt, ist, eine Hilfsfunktion is_any_of zu schreiben:


enum state_type { IDLE, CONNECTED, DISCONNECTED };

template <typename ... Ts>
bool is_any_of(state_type s, const Ts& ... ts) { 
    return ((s == ts) || ...); 
}

Dieses Zwischenergebnis ist enttäuschend. Bisher wird der Code nicht besser lesbar:

assert(is_any_of(state, IDLE, DISCONNECTING, DISCONNECTED)); 

Nicht-typspezifische Vorlagenparameter werden dazu beitragen, die Situation ein wenig zu verbessern. Mit ihrer Hilfe übertragen wir die aufzählbaren Elemente der Aufzählung in die Liste der Template-Parameter: 

template <state_type ... states>
bool is_any_of(state_type t) { 
    return ((t == states) | ...); 
}
	
assert(is_any_of<IDLE, DISCONNECTING, DISCONNECTED>(state)); 

Durch die Verwendung von auto in einem Nicht-Typ-Vorlagenparameter (C++17) lässt sich der Ansatz einfach auf Vergleiche nicht nur mit state_type-Elementen verallgemeinern, sondern auch mit primitiven Typen, die als Nicht-Typ-Vorlagenparameter verwendet werden können:


template <auto ... alternatives, typename T>
bool is_any_of(const T& t) {
    return ((t == alternatives) | ...);
}

Durch diese sukzessiven Verbesserungen wird die gewünschte flüssige Syntax für Prüfungen erreicht:


template <class ... Ts>
struct any_of : private std::tuple<Ts ...> { 
// поленимся и унаследуем конструкторы от tuple 
        using std::tuple<Ts ...>::tuple;
        template <typename T>
        bool operator ==(const T& t) const {
                return std::apply(
                        [&t](const auto& ... ts) {
                                return ((ts == t) || ...);
                        },
                        static_cast<const std::tuple<Ts ...>&>(*this));
        }
};

template <class ... Ts>
any_of(Ts ...) -> any_of<Ts ... >;
 
assert(any_of(IDLE, DISCONNECTING, DISCONNECTED) == state);

In diesem Beispiel dient der Ableitungsleitfaden dazu, dem Compiler, der die Typen der Konstruktorargumente kennt, die gewünschten Strukturvorlagenparameter vorzuschlagen. 

Weiter - interessanter. Bjorn lehrt, wie man den resultierenden Code für Vergleichsoperatoren über == hinaus und dann für beliebige Operationen verallgemeinert. Nebenbei werden Features wie das Attribut no_unique_address (C++20) und Template-Parameter in Lambda-Funktionen (C++20) anhand von Anwendungsbeispielen erläutert. (Ja, jetzt ist die Lambda-Syntax noch einfacher zu merken – das sind vier aufeinanderfolgende Klammerpaare aller Art.) Die endgültige Lösung, die Funktionen als Konstruktordetails verwendet, wärmt meine Seele wirklich, ganz zu schweigen vom Ausdruck Tupel in den besten Traditionen von Lambda Infinitesimalrechnung.

Vergessen Sie am Ende nicht, es aufzupolieren:

  • Denken Sie daran, dass Lambdas kostenlos constexpr sind. 
  • Fügen wir die perfekte Weiterleitung hinzu und schauen uns die hässliche Syntax in Bezug auf das Parameterpaket im Lambda-Abschluss an.
  • Geben wir dem Compiler mehr Möglichkeiten für Optimierungen mit bedingtem „noexclusive“; 
  • Sorgen wir dank expliziter Rückgabewerte von Lambdas für eine verständlichere Fehlerausgabe in Vorlagen. Dadurch wird der Compiler gezwungen, weitere Prüfungen durchzuführen, bevor die Vorlagenfunktion tatsächlich aufgerufen wird – in der Phase der Typprüfung. 

Einzelheiten entnehmen Sie bitte den Vorlesungsmaterialien: 

Unsere Eindrücke

Unsere erste Teilnahme an C++ Russia war wegen ihrer Intensität unvergesslich. Ich hatte den Eindruck, dass C++ Russia eine ernsthafte Veranstaltung war, bei der die Grenze zwischen Schulung und Live-Kommunikation kaum wahrnehmbar ist. Von der Stimmung der Redner bis hin zu den Gewinnspielen der Veranstaltungspartner ist alles für hitzige Diskussionen geeignet. Der Inhalt der Konferenz, bestehend aus Berichten, deckt ein recht breites Themenspektrum ab, darunter C++-Innovationen, Fallstudien großer Projekte und ideologische Architekturüberlegungen. Aber es wäre unfair, die soziale Komponente der Veranstaltung zu ignorieren, die dabei hilft, Sprachbarrieren nicht nur in Bezug auf C++ zu überwinden.

Wir danken den Organisatoren der Konferenz für die Möglichkeit, an einer solchen Veranstaltung teilzunehmen!
Möglicherweise haben Sie den Beitrag der Organisatoren über die Vergangenheit, Gegenwart und Zukunft von C++ Russia gesehen auf dem JUG Ru-Blog.

Vielen Dank fürs Lesen und wir hoffen, dass unsere Nacherzählung der Ereignisse hilfreich war!

Source: habr.com

Kommentar hinzufügen