ref Einheimische und ref kehrt in C # zurück: Leistungsprobleme

Von Anfang an unterstützte C # die Übergabe von Argumenten nach Wert oder Referenz. Vor Version 7 unterstützte der C # -Compiler jedoch nur eine Möglichkeit, einen Wert von einer Methode (oder Eigenschaft) zurückzugeben - die Rückgabe nach Wert. In C # 7 hat sich die Situation mit der Einführung von zwei neuen Funktionen geändert: Ref Returns und Ref Locals. Mehr über sie und ihre Leistung - unter dem Schnitt.



Gründe


Es gibt viele Unterschiede zwischen Arrays und anderen Sammlungen hinsichtlich der Laufzeit der gemeinsamen Sprache. Die CLR unterstützte von Anfang an Arrays und kann als integrierte Funktionalität betrachtet werden. Die CLR-Umgebung und der JIT-Compiler können mit Arrays arbeiten, und sie haben noch eine weitere Funktion: Der Array-Indexer gibt Elemente als Referenz und nicht nach Wert zurück.

Um dies zu demonstrieren, müssen wir uns der verbotenen Methode zuwenden - verwenden Sie den veränderlichen Werttyp:

public struct Mutable { private int _x; public Mutable(int x) => _x = x; public int X => _x; public void IncrementX() { _x++; } } [Test] public void CheckMutability() { var ma = new[] {new Mutable(1)}; ma[0].IncrementX(); // X has been changed! Assert.That(ma[0].X, Is.EqualTo(2)); var ml = new List<Mutable> {new Mutable(1)}; ml[0].IncrementX(); // X hasn't been changed! Assert.That(ml[0].X, Is.EqualTo(1)); } 

Das Testen ist erfolgreich, da sich der Array-Indexer erheblich vom Listen-Indexer unterscheidet.

Der C # -Compiler gibt dem Array-Indexer - ldelema - eine spezielle Anweisung, die einen verwalteten Link zu einem Element dieses Arrays zurückgibt. Im Wesentlichen gibt ein Array-Indexer ein Element als Referenz zurück. List kann sich jedoch nicht auf die gleiche Weise verhalten, da in C # * kein Alias ​​des internen Status zurückgegeben werden konnte. Daher gibt der Listenindexer ein Element nach Wert zurück, dh eine Kopie dieses Elements.

* Wie wir bald sehen werden, kann der Listenindexer immer noch kein Element als Referenz zurückgeben.

Dies bedeutet, dass ma [0] .IncrementX () die Methode aufruft, die das erste Element des Arrays ändert, während ml [0] .IncrementX () die Methode aufruft, die die Kopie des Elements ändert, ohne die ursprüngliche Liste zu beeinflussen.

Rückgabewerte und lokale Referenzvariablen: Grundlagen


Die Bedeutung dieser Funktionen ist sehr einfach: Wenn Sie den zurückgegebenen Referenzwert deklarieren, können Sie den Alias ​​einer vorhandenen Variablen zurückgeben, und die lokale Referenzvariable kann einen solchen Alias ​​speichern.

1. Ein einfaches Beispiel:

 [Test] public void RefLocalsAndRefReturnsBasics() { int[] array = { 1, 2 }; // Capture an alias to the first element into a local ref int first = ref array[0]; first = 42; Assert.That(array[0], Is.EqualTo(42)); // Local function that returns the first element by ref ref int GetByRef(int[] a) => ref a[0]; // Weird syntax: the result of a function call is assignable GetByRef(array) = -1; Assert.That(array[0], Is.EqualTo(-1)); } 

2. Zurückgegebene Referenzwerte und schreibgeschützter Modifikator

Der zurückgegebene Referenzwert kann den Alias ​​des Instanzfelds zurückgeben. Ab C # Version 7.2 können Sie den Alias ​​zurückgeben, ohne mit dem Modifikator ref readonly in das entsprechende Objekt schreiben zu können:

 class EncapsulationWentWrong { private readonly Guid _guid; private int _x; public EncapsulationWentWrong(int x) => _x = x; // Return an alias to the private field. No encapsulation any more. public ref int X => ref _x; // Return a readonly alias to the private field. public ref readonly Guid Guid => ref _guid; } [Test] public void NoEncapsulation() { var instance = new EncapsulationWentWrong(42); instance.X++; Assert.That(instance.X, Is.EqualTo(43)); // Cannot assign to property 'EncapsulationWentWrong.Guid' because it is a readonly variable // instance.Guid = Guid.Empty; } 

  • Methoden und Eigenschaften können einen „Alias“ des internen Status zurückgeben. In diesem Fall darf die Taskmethode nicht für die Eigenschaft definiert werden.
  • Die Rückgabe per Referenz unterbricht die Kapselung, da der Client die volle Kontrolle über den internen Status des Objekts erlangt.
  • Durch die Rückgabe über eine schreibgeschützte Verbindung wird vermieden, dass Werttypen unnötig kopiert werden, während der Client den internen Status nicht ändern kann.
  • Schreibgeschützte Links können für Referenztypen verwendet werden, obwohl dies in nicht standardmäßigen Fällen wenig sinnvoll ist.

3. Bestehende Einschränkungen. Das Zurückgeben eines Alias ​​kann gefährlich sein: Die Verwendung eines Alias ​​für eine Variable, die nach Abschluss der Methode auf dem Stapel abgelegt wird, führt zum Absturz der Anwendung. Um diese Funktion sicher zu machen, wendet der C # -Compiler verschiedene Einschränkungen an:

  • Link zur lokalen Variablen kann nicht zurückgegeben werden.
  • In Strukturen kann kein Verweis darauf zurückgegeben werden.
  • Sie können einen Link zu einer Variablen auf dem Heap zurückgeben (z. B. zu einem Klassenmitglied).
  • Sie können einen Link zu den Ref / Out-Parametern zurückgeben.

Für weitere Informationen empfehlen wir Ihnen, die ausgezeichnete Veröffentlichung Safe to return-Regeln für Ref-Rücksendungen zu lesen . Der Autor des Artikels, Vladimir Sadov, ist der Ersteller der Rückgabereferenzfunktion für den C # -Compiler.

Nachdem wir nun eine allgemeine Vorstellung von zurückgegebenen Referenzwerten und referenzierten lokalen Variablen haben, schauen wir uns an, wie sie verwendet werden können.

Zurückgegebene Referenzwerte in Indexern verwenden


Um die Auswirkungen dieser Funktionen auf die Leistung zu testen, erstellen wir eine einzigartige, unveränderliche Sammlung mit dem Namen NaiveImmutableList <T> und vergleichen sie mit T [] und List für Strukturen unterschiedlicher Größe (4, 16, 32 und 48).

 public class NaiveImmutableList<T> { private readonly int _length; private readonly T[] _data; public NaiveImmutableList(params T[] data) => (_data, _length) = (data, data.Length); public ref readonly T this[int idx] // R# 2017.3.2 is completely confused with this syntax! // => ref (idx >= _length ? ref Throw() : ref _data[idx]); { get { // Extracting 'throw' statement into a different // method helps the jitter to inline a property access. if ((uint)idx >= (uint)_length) ThrowIndexOutOfRangeException(); return ref _data[idx]; } } private static void ThrowIndexOutOfRangeException() => throw new IndexOutOfRangeException(); } struct LargeStruct_48 { public int N { get; } private readonly long l1, l2, l3, l4, l5; public LargeStruct_48(int n) : this() => N = n; } // Other structs like LargeStruct_16, LargeStruct_32 etc 

Ein Leistungstest wird für alle Sammlungen durchgeführt und addiert alle N Eigenschaftswerte für jedes Element:

 private const int elementsCount = 100_000; private static LargeStruct_48[] CreateArray_48() => Enumerable.Range(1, elementsCount).Select(v => new LargeStruct_48(v)).ToArray(); private readonly LargeStruct_48[] _array48 = CreateArray_48(); [BenchmarkCategory("BigStruct_48")] [Benchmark(Baseline = true)] public int TestArray_48() { int result = 0; // Using elementsCound but not array.Length to force the bounds check // on each iteration. for (int i = 0; i < elementsCount; i++) { result = _array48[i].N; } return result; } 

Die Ergebnisse sind wie folgt:



Anscheinend stimmt etwas nicht! Die Leistung unserer NaiveImmutableList <T> -Sammlung entspricht der Liste. Was ist passiert?

Rückgabewerte mit schreibgeschütztem Modifikator: So funktioniert es


Wie Sie sehen können, gibt der NaiveImmutableList <T> -Indexer mithilfe des Modifikators ref readonly einen schreibgeschützten Link zurück. Dies ist völlig gerechtfertigt, da wir die Fähigkeit der Kunden einschränken möchten, den zugrunde liegenden Status einer unveränderlichen Sammlung zu ändern. Die Strukturen, die wir im Leistungstest verwenden, sind jedoch nicht nur lesbar.

Dieser Test hilft uns, das grundlegende Verhalten zu verstehen:

 [Test] public void CheckMutabilityForNaiveImmutableList() { var ml = new NaiveImmutableList<Mutable>(new Mutable(1)); ml[0].IncrementX(); // X has been changed, right? Assert.That(ml[0].X, Is.EqualTo(2)); } 

Der Test ist fehlgeschlagen! Aber warum? Da die Struktur von "Nur-Lese-Links" der Struktur von in Modifikatoren und schreibgeschützten Feldern in Bezug auf Strukturen ähnlich ist: Der Compiler generiert bei jeder Verwendung eines Strukturelements eine Schutzkopie. Dies bedeutet, dass ml [0]. erstellt weiterhin eine Kopie des ersten Elements, dies wird jedoch nicht vom Indexer ausgeführt: Die Kopie wird am Aufrufpunkt erstellt.

Dieses Verhalten macht tatsächlich Sinn. Der C # -Compiler unterstützt die Übergabe von Argumenten nach Wert, Referenz und "Nur-Lese-Link" unter Verwendung des In-Modifikators (Einzelheiten finden Sie unter Der In-Modifikator und die schreibgeschützten Strukturen in C # ("Der In-Modifikator und die Nur-Lese-Strukturen in C #") ")). Jetzt unterstützt der Compiler drei verschiedene Möglichkeiten, einen Wert von einer Methode zurückzugeben: nach Wert, nach Referenz und nach schreibgeschütztem Link.

Schreibgeschützte Links sind regulären Links so ähnlich, dass der Compiler dasselbe InAttribute verwendet, um zwischen ihren Rückgabewerten zu unterscheiden:

 private int _n; public ref readonly int ByReadonlyRef() => ref _n; 

In diesem Fall wird die ByReadonlyRef-Methode effizient kompiliert in:

 [InAttribute] [return: IsReadOnly] public int* ByReadonlyRef() { return ref this._n; } 

Die Ähnlichkeit zwischen dem Modifikator in und der schreibgeschützten Verbindung bedeutet, dass diese Funktionen für reguläre Strukturen nicht sehr geeignet sind und Leistungsprobleme verursachen können. Betrachten Sie ein Beispiel:

 public struct BigStruct { // Other fields public int X { get; } public int Y { get; } } private BigStruct _bigStruct; public ref readonly BigStruct GetBigStructByRef() => ref _bigStruct; ref readonly var bigStruct = ref GetBigStructByRef(); int result = bigStruct.X + bigStruct.Y; 

Neben der ungewöhnlichen Syntax beim Deklarieren einer Variablen für bigStruct sieht der Code gut aus. Das Ziel ist klar: BigStruct kehrt aus Leistungsgründen als Referenz zurück. Da die BigStruct-Struktur beschreibbar ist, wird bei jedem Zugriff auf das Element leider eine Schutzkopie erstellt.

Zurückgegebene Referenzwerte in Indexern verwenden. Versuch Nummer 2


Versuchen wir die gleichen Tests für schreibgeschützte Strukturen unterschiedlicher Größe:



Jetzt sind die Ergebnisse viel sinnvoller. Die Verarbeitungszeit für große Strukturen nimmt immer noch zu, dies wird jedoch erwartet, da die Verarbeitung von mehr als 100.000 größeren Strukturen länger dauert. Aber jetzt ist die Laufzeit für NaiveimmutableList <T> sehr nahe an der Zeit T [] und viel besser als im Fall von List.

Fazit


  • Zurückgegebene Referenzwerte sollten sorgfältig behandelt werden, da sie die Kapselung unterbrechen können.
  • Zurückgegebene Referenzwerte mit schreibgeschütztem Modifikator sind nur für schreibgeschützte Strukturen wirksam. Bei herkömmlichen Strukturen können Leistungsprobleme auftreten.
  • Bei der Arbeit mit beschreibbaren Strukturen erstellen zurückgegebene Referenzwerte mit dem schreibgeschützten Modifikator bei jeder Verwendung der Variablen eine Schutzkopie, was zu Leistungsproblemen führen kann.

Zurückgegebene Referenzwerte und referenzierte lokale Variablen sind nützliche Funktionen für Bibliotheksersteller und Entwickler von Infrastrukturcode. Die Verwendung im Bibliothekscode ist jedoch sehr gefährlich: Um eine Sammlung zu verwenden, die Elemente mithilfe eines schreibgeschützten Links effektiv zurückgibt, muss sich jeder Bibliotheksbenutzer daran erinnern: Ein schreibgeschützter Link zu einer beschreibbaren Struktur erstellt eine Schutzkopie „am Aufrufpunkt ". Im besten Fall wird dies eine mögliche Steigerung der Produktivität zunichte machen, und im schlimmsten Fall wird dies zu einer ernsthaften Verschlechterung führen, wenn gleichzeitig eine große Anzahl von Anforderungen an eine lokale Referenzvariable schreibgeschützt gestellt wird.

PS Schreibgeschützte Links werden in BCL angezeigt. Die schreibgeschützten Referenzmethoden für den Zugriff auf Elemente in unveränderlichen Sammlungen wurden in der folgenden Anforderung vorgestellt, um die Änderungen in corefx repo ( Implementieren des ItemRef-API-Vorschlags („Vorschlag zum Einschließen der ItemRef-API“)) aufzunehmen. Daher ist es sehr wichtig, dass jeder die Funktionen dieser Funktionen versteht und weiß, wie und wann sie angewendet werden sollten.

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


All Articles