Struktur und schreibgeschützt: So vermeiden Sie Leistungseinbußen

Die Verwendung des Strukturtyps und des schreibgeschützten Modifikators kann manchmal zu Leistungseinbußen führen. Heute werden wir darüber sprechen, wie dies mit einem Open Source Code Analyzer - ErrorProne.NET - vermieden werden kann.



Wie Sie wahrscheinlich aus meinen früheren Veröffentlichungen wissen, " Der 'in'-Modifikator und die schreibgeschützten Strukturen in C # " ("Der Modifikator in und schreibgeschützte Strukturen in C #") und " Leistungsfallen von Ref-Einheimischen und Ref-Rückgaben in C # " (" Leistungsfallen bei Verwendung lokaler Variablen und Rückgabewerte mit dem Modifikator ref)) ist das Arbeiten mit Strukturen schwieriger, als es den Anschein hat. Abgesehen von der Frage der Veränderlichkeit stelle ich fest, dass das Verhalten von Strukturen mit schreibgeschütztem Modifikator (schreibgeschützt) und ohne diesen in schreibgeschützten Kontexten sehr unterschiedlich ist.

Es wird davon ausgegangen, dass Strukturen in Programmierskripten verwendet werden, die eine hohe Leistung erfordern. Um effektiv mit ihnen arbeiten zu können, sollten Sie etwas über die verschiedenen versteckten Operationen wissen, die vom Compiler generiert werden, um sicherzustellen, dass die Struktur unverändert bleibt.

Hier ist eine kurze Liste von Vorsichtsmaßnahmen, an die Sie sich erinnern sollten:

  • Die Verwendung großer Strukturen, die als Wert übergeben oder zurückgegeben werden, kann zu Leistungsproblemen bei kritischen Programmausführungspfaden führen.
  • xY bewirkt, dass eine Schutzkopie von x erstellt wird, wenn:
    • x ist ein schreibgeschütztes Feld;
    • Typ x ist eine Struktur ohne schreibgeschützten Modifikator.
    • Y ist kein Feld.

Dieselben Regeln gelten, wenn x ein Parameter mit dem Modifikator in, eine lokale Variable mit dem Modifikator ref readonly oder das Ergebnis des Aufrufs einer Methode ist, die einen Wert über eine schreibgeschützte Referenz zurückgibt.

Hier sind einige Regeln, die Sie beachten sollten. Und vor allem ist der Code, der sich auf diese Regeln stützt, sehr fragil (dh Änderungen am Code führen sofort zu signifikanten Änderungen in anderen Teilen des Codes oder der Dokumentation - ca. übersetzt). Wie viele Leute werden bemerken, dass das Ersetzen von public readonly int X ; on public int X { get; } public int X { get; } in einer häufig verwendeten Struktur ohne schreibgeschützten Modifikator die Leistung erheblich beeinträchtigen? Oder wie einfach ist es zu erkennen, dass die Übergabe eines Parameters mit dem Modifikator in anstelle der Übergabe nach Wert die Leistung beeinträchtigen kann? Dies ist wirklich möglich, wenn die in-Eigenschaft eines Parameters in einer Schleife verwendet wird, wenn bei jeder Iteration eine Schutzkopie erstellt wird.

Solche Eigenschaften von Strukturen sprechen buchstäblich die Entwicklung von Analysatoren an. Und der Anruf wurde gehört. Lernen Sie ErrorProne.NET kennen - eine Reihe von Analysegeräten, die Sie über die Möglichkeit informieren, Programmcode zu ändern, um dessen Design und Leistung bei der Arbeit mit Strukturen zu verbessern.

Code-Analyse mit Nachrichtenausgabe "Machen Sie die X-Struktur schreibgeschützt"


Der beste Weg, um subtile Fehler und negative Auswirkungen auf die Leistung bei der Verwendung von Strukturen zu vermeiden, besteht darin, sie nach Möglichkeit schreibgeschützt zu machen. Der schreibgeschützte Modifikator in der Strukturdeklaration drückt eindeutig die Absicht des Entwicklers aus (wobei betont wird, dass die Struktur unveränderlich ist) und hilft dem Compiler, das Laichen von Sicherheitskopien in vielen der oben genannten Kontexte zu vermeiden.



Das Deklarieren einer schreibgeschützten Struktur verletzt die Code-Integrität nicht. Sie können den Fixer (den Vorgang des Fixierens des Codes) sicher im Batch-Modus ausführen und alle Strukturen der gesamten Softwarelösung als schreibgeschützt deklarieren.

Freundlichkeit für ref readonly Modifikator


Der nächste Schritt besteht darin, die Sicherheit der Verwendung neuer Funktionen (in Modifikatoren, lokalen Lesevariablen, Referenzvariablen usw.) zu bewerten. Dies bedeutet, dass der Compiler keine versteckten Schutzkopien erstellt, die die Leistung beeinträchtigen können.

Drei Arten von Typen können betrachtet werden:

  • ref schreibgeschützte Strukturen, deren Verwendung niemals zur Erstellung von Schutzkopien führt;
  • Strukturen, die nicht schreibgeschützt sind, deren Verwendung im Zusammenhang mit schreibgeschützt immer zur Erstellung von Schutzkopien führt;
  • neutrale Strukturen - Strukturen, deren Verwendung je nach dem im schreibgeschützten Kontext verwendeten Element zu Schutzkopien führen kann.

Die erste Kategorie umfasst schreibgeschützte Strukturen und POCO-Strukturen. Der Compiler generiert niemals eine Schutzkopie, wenn die Struktur schreibgeschützt ist. Es ist auch sicher, POCO-Strukturen im Zusammenhang mit Readonly zu verwenden: Der Zugriff auf Felder wird als sicher angesehen, und es werden keine Schutzkopien erstellt.

Die zweite Kategorie sind Strukturen ohne schreibgeschützten Modifikator, die keine offenen Felder enthalten. In diesem Fall führt jeder Zugriff auf das öffentliche Mitglied im schreibgeschützten Kontext zur Erstellung einer Schutzkopie.

Die letzte Kategorie sind Strukturen mit öffentlichen oder internen Feldern und öffentlichen oder internen Eigenschaften oder Methoden. In diesem Fall erstellt der Compiler je nach verwendetem Mitglied Schutzkopien.

Diese Trennung hilft, sofort Warnungen zu generieren, wenn die "unfreundliche" Struktur mit dem Modifikator in übergeben wird, der in der lokalen Variablen ref readonly usw. gespeichert ist.



Der Analysator zeigt keine Warnung an, wenn die "unfreundliche" Struktur als schreibgeschütztes Feld verwendet wird, da es in diesem Fall keine Alternative gibt. Die schreibgeschützten In- und Ref-Modifikatoren wurden speziell optimiert, um das Erstellen redundanter Kopien zu vermeiden. Wenn die Struktur in Bezug auf diese Modifikatoren "unfreundlich" ist, haben Sie andere Möglichkeiten: Übergeben Sie ein Argument als Wert oder speichern Sie eine Kopie in einer lokalen Variablen. In dieser Hinsicht verhalten sich schreibgeschützte Felder anders: Wenn Sie den Typ unveränderlich machen möchten, müssen Sie diese Felder verwenden. Denken Sie daran: Der Code muss klar und elegant sein und nur sekundär schnell.

Bcc-Analyse


Der Compiler führt viele Aktionen aus, die dem Benutzer verborgen bleiben. Wie in einem vorherigen Beitrag gezeigt , ist es ziemlich schwer zu erkennen, wann eine Schutzkopie erstellt wird.

Der Analysator erkennt die folgenden versteckten Kopien:

  1. Bcc des schreibgeschützten Feldes.
  2. Bcc von in.
  3. Bcc der schreibgeschützten lokalen Variablen ref.
  4. Bcc return ref schreibgeschützt.
  5. Bcc beim Aufrufen einer Erweiterungsmethode, die einen Parameter mit diesem Modifikator als Wert für eine Instanz der Struktur verwendet.

 public struct NonReadOnlyStruct { public readonly long PublicField; public long PublicProperty { get; } public void PublicMethod() { } private static readonly NonReadOnlyStruct _ros; public static void Samples(in NonReadOnlyStruct nrs) { // Ok. Public field access causes no hidden copies var x = nrs.PublicField; // Ok. No hidden copies. x = _ros.PublicField; // Hidden copy: Property access on 'in'-parameter x = nrs.PublicProperty; // Hidden copy: Method call on readonly field _ros.PublicMethod(); ref readonly var local = ref nrs; // Hidden copy: method call on ref readonly local local.PublicMethod(); // Hidden copy: method call on ref readonly return Local().PublicMethod(); ref readonly NonReadOnlyStruct Local() => ref _ros; } } 

Bitte beachten Sie, dass die Analysegeräte Diagnosemeldungen nur anzeigen, wenn die Strukturgröße ≥ 16 Byte beträgt.

Verwendung von Analysatoren in realen Projekten


Die Übertragung großer Strukturen nach Wert und damit die Erstellung von Schutzkopien durch den Compiler beeinträchtigen die Leistung erheblich. Zumindest zeigen dies die Ergebnisse von Leistungstests. Aber wie wirken sich diese Phänomene auf reale Anwendungen in Bezug auf die End-to-End-Zeit aus?

Um die Analysatoren mit echtem Code zu testen, habe ich sie für zwei Projekte verwendet: das Roslyn-Projekt und das interne Projekt, an dem ich derzeit bei Microsoft arbeite (das Projekt ist eine eigenständige Computeranwendung mit strengen Leistungsanforderungen); Nennen wir es der Klarheit halber "Projekt D".

Hier sind die Ergebnisse:

  1. Projekte mit hohen Leistungsanforderungen enthalten normalerweise viele Strukturen, von denen die meisten schreibgeschützt sind. Beispielsweise fand der Analysator im Roslyn-Projekt ungefähr 400 Strukturen, die schreibgeschützt werden können, und im D-Projekt ungefähr 300.
  2. In Projekten mit hohen Leistungsanforderungen sollten Blindkopien nur in Ausnahmesituationen erstellt werden. Ich habe im Roslyn-Projekt nur wenige solcher Fälle gefunden, da die meisten Strukturen öffentliche Felder anstelle von öffentlichen Grundstücken haben. Dadurch wird vermieden, dass Schutzkopien in Situationen erstellt werden, in denen Strukturen in schreibgeschützten Feldern gespeichert sind. In Projekt D gab es mehr Blindkopien, da mindestens die Hälfte von ihnen Nur-Get-Eigenschaften hatte (Nur-Lese-Zugriff).
  3. Die Übertragung selbst ziemlich großer Strukturen unter Verwendung des In-Modifikators hat wahrscheinlich nur einen sehr geringen (fast nicht wahrnehmbaren) Einfluss auf die Durchlaufzeit des Programms.

Ich habe alle 300 Strukturen in Projekt D geändert, sie schreibgeschützt gemacht und dann Hunderte von Fällen ihrer Verwendung korrigiert, um anzuzeigen, dass sie mit dem Modifikator in übergeben wurden. Dann habe ich die End-to-End-Transitzeit für verschiedene Leistungsszenarien gemessen. Die Unterschiede waren statistisch nicht signifikant.

Bedeutet dies, dass die oben beschriebenen Funktionen unbrauchbar sind? Überhaupt nicht.

Die Arbeit an einem Projekt mit hohen Leistungsanforderungen (z. B. an Roslyn oder „Projekt D“) impliziert, dass eine große Anzahl von Personen viel Zeit mit verschiedenen Arten der Optimierung verbringt. In einigen Fällen wurden Strukturen in unserem Code sogar mit dem ref-Modifikator übergeben, und einige Felder wurden ohne den schreibgeschützten Modifikator deklariert, um die Erzeugung von Schutzkopien auszuschließen. Das fehlende Produktivitätswachstum während der Übertragung von Strukturen mit dem Modifikator in kann dazu führen, dass der Code gut optimiert wurde und keine übermäßigen Kopien von Strukturen auf den kritischen Pfaden seines Durchgangs vorhanden sind.

Was soll ich mit diesen Funktionen tun?


Ich glaube, dass das Problem der Verwendung des schreibgeschützten Modifikators für Strukturen nicht viel Nachdenken erfordert. Wenn die Struktur unveränderlich ist, zwingt der schreibgeschützte Modifikator den Compiler einfach explizit zu einer solchen Entwurfsentscheidung. Und das Fehlen von Schutzkopien für solche Strukturen ist nur ein Bonus.

Heute lauten meine Empfehlungen wie folgt: Wenn die Struktur nur lesbar ist, dann machen Sie es auf jeden Fall so.

Die Verwendung der anderen berücksichtigten Optionen weist Nuancen auf.

Voroptimierung versus Vorpessimierung?


Herb Sutter führt das Konzept der „vorläufigen Pessimisierung“ in seinem erstaunlichen Buch C ++ Coding Standards: 101 Rule, Recommendations and Best Practices ein .

„Ceteris paribus, Codekomplexität und Lesbarkeit, einige effektive Entwurfsmuster und Codierungssprachen sollten natürlich von Ihren Fingerspitzen abfließen. Ein solcher Code ist nicht schwieriger zu schreiben als seine pessimierten Alternativen. Sie führen keine vorläufige Optimierung durch, vermeiden jedoch eine freiwillige Pessimisierung. “

Aus meiner Sicht ist ein Parameter mit dem Modifikator in genau der Fall. Wenn Sie wissen, dass die Struktur relativ groß ist (40 Byte oder mehr), können Sie sie jederzeit mit dem Modifikator in übergeben. Die Kosten für die Verwendung des Modifikators in sind relativ gering, da Sie die Anrufe nicht anpassen müssen und die Vorteile real sein können.

Im Gegensatz dazu ist dies bei lokalen Variablen und Rückgabewerten mit dem Modifikator readonly ref nicht der Fall. Ich würde sagen, dass diese Funktionen beim Codieren von Bibliotheken verwendet werden sollten, und es ist besser, sie im Anwendungscode abzulehnen (nur wenn die Profilerstellung des Codes nicht ergibt, dass der Kopiervorgang wirklich ein Problem darstellt). Die Verwendung dieser Funktionen erfordert zusätzlichen Aufwand und es wird für den Codeleser schwieriger, sie zu verstehen.

Fazit


  1. Verwenden Sie nach Möglichkeit den Readonly-Modifikator für Strukturen.
  2. Verwenden Sie den Modifikator in für große Strukturen.
  3. Erwägen Sie die Verwendung lokaler Variablen und Rückgabewerte mit dem Modifikator ref readonly, um Bibliotheken zu codieren, oder in Fällen, in denen die Ergebnisse der Codeprofilerstellung darauf hinweisen, dass dies nützlich sein kann.
  4. Verwenden Sie ErrorProne.NET , um Codeprobleme zu erkennen und die Ergebnisse freizugeben .

Source: https://habr.com/ru/post/de423053/


All Articles