ref Einheimische und ref Rückkehr in C #: Performance-Fallstricke

Ursprünglicher Autor: Sergey Teplyakov
  • Übersetzung
In C # wurde von Anfang an die Übergabe von Argumenten nach Wert oder Referenz unterstützt. Vor Version 7 unterstützte der C # -Compiler jedoch nur eine Möglichkeit, einen Wert von einer Methode (oder Eigenschaft) zurückzugeben: 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 Strich.



Gründe


Es gibt viele Unterschiede zwischen Arrays und anderen Auflistungen in Bezug auf die Common Language Runtime. Die CLR unterstützte von Anfang an Arrays und diese können als integrierte Funktionalität betrachtet werden. Die CLR-Umgebung und der JIT-Compiler können mit Arrays arbeiten, und sie verfügen außerdem über eine weitere Funktion: Der Array-Indexer gibt Elemente nach Verweis und nicht nach Wert zurück.

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

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 {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 eine spezielle Anweisung - ldelema, 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 es in C # nicht möglich war, einen Alias ​​des internen Status zurückzugeben. Daher gibt der Listenindexer ein Element nach Wert zurück, dh eine Kopie dieses Elements.

* Wie wir gleich sehen werden, kann der Listenindexer immer noch keine Artikel 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 readonly-Modifikator Der

zurückgegebene Referenzwert kann einen Alias ​​eines Instanzfelds zurückgeben. Ab C # Version 7.2 können Sie einen Alias ​​zurückgeben, ohne mit dem readonly-Modifikator ref 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 Zustands zurückgeben. In diesem Fall darf die Task-Methode für die Eigenschaft nicht definiert werden.
  • Wenn Sie per Referenz zurückgeben, wird die Kapselung unterbrochen, da der Client die vollständige Kontrolle über den internen Zustand des Objekts erlangt.
  • Die Rückgabe über eine Nur-Lese-Verknüpfung vermeidet das unnötige Kopieren von Werttypen, ohne dass der Client den internen Status ä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 zurückgeben, die sich auf dem Heap befindet (z. B. zu einem Klassenmitglied).
  • Sie können einen Link zu den ref / out-Parametern zurückgeben.

Für weitere Informationen empfehlen wir, dass Sie die ausgezeichnete Publikation Safe to return rules for ref returns lesen . Der Autor des Artikels, Vladimir Sadov, ist der Schöpfer der Funktion zum Zurückgeben von Referenzwerten für den C # -Compiler.

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

Verwenden von zurückgegebenen Referenzwerten in Indexern


Um die Auswirkung dieser Funktionen auf die Leistung zu testen, erstellen wir eine eindeutige unveränderliche Auflistung 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
{
    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> -Kollektion entspricht der von List. Was ist passiert?

Rückgabewerte mit dem readonly-Modifikator: Funktionsweise


Wie Sie sehen, gibt der Indexer NaiveImmutableList <T> einen schreibgeschützten Link mit dem Modifikator ref readonly zurück. Dies ist völlig gerechtfertigt, da wir die Möglichkeit der Kunden einschränken möchten, den Grundzustand 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(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 ähnelt: Der Compiler generiert bei jeder Verwendung eines Strukturelements eine Schutzkopie. Dies bedeutet, dass ml [0]. Es wird immer noch eine Kopie des ersten Elements erstellt, dies wird jedoch nicht vom Indexer ausgeführt: Eine 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-Verknüpfung" mit dem Modifikator in (Einzelheiten finden Sie unter Der Modifikator in und die schreibgeschützten Strukturen in C #.("Die In-Modifikator- und Nur-Lese-Strukturen in C #"). Der Compiler unterstützt jetzt drei verschiedene Möglichkeiten, einen Wert von einer Methode zurückzugeben: nach Wert, nach Verweis 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 kompiliert die ByReadonlyRef-Methode effizient in:

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

Die Ähnlichkeit zwischen dem in-Modifikator und der Nur-Lese-Verknüpfung 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;

Abgesehen von 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 leider bei jedem Zugriff auf das Element eine Schutzkopie erstellt.

Verwenden von zurückgegebenen Referenzwerten in Indexern. Versuch Nummer 2


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



Jetzt sind die Ergebnisse viel sinnvoller. Bei großen Strukturen nimmt die Verarbeitungszeit immer noch zu, dies wird jedoch erwartet, da die Verarbeitung von mehr als 100.000 größeren Strukturen länger dauert. 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 beschädigen können.
  • Zurückgegebene Referenzwerte mit dem Modifikator readonly 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 Modifikator readonly 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 über einen Nur-Lese-Link zurückgibt, muss jeder Bibliotheksbenutzer Folgendes beachten: Der Nur-Lese-Link zur beschreibbaren Struktur erstellt eine Schutzkopie “am Aufrufpunkt ". Im besten Fall wird dies eine mögliche Produktivitätssteigerung zunichte machen und im schlimmsten Fall zu einer ernsthaften Verschlechterung führen, wenn gleichzeitig eine große Anzahl von Anfragen an eine lokale Referenzvariable gestellt wird, die schreibgeschützt ist.

PS In BCL werden schreibgeschützte Links angezeigt. Die schreibgeschützten Ref-Methoden für den Zugriff auf Elemente in unveränderlichen Auflistungen wurden in der folgenden Anfrage vorgestellt, um die Änderungen in corefx repo zu übernehmen ( Implementing ItemRef API Proposal („Angebot zur Aufnahme der ItemRef-API“)). Daher ist es sehr wichtig, dass jeder die Merkmale der Verwendung dieser Funktionen versteht und weiß, wie und wann sie angewendet werden sollten.

Jetzt auch beliebt: