Warum ist TypedReference hinter den Kulissen? Es ist so schnell und sicher ... fast magisch!

128

Warnung: Diese Frage ist etwas ketzerisch ... religiöse Programmierer halten sich immer an gute Praktiken, bitte lesen Sie sie nicht. :) :)

Weiß jemand, warum von der Verwendung von TypedReference so abgeraten wird (implizit aufgrund fehlender Dokumentation)?

Ich habe großartige Verwendungsmöglichkeiten dafür gefunden, z. B. wenn generische Parameter über Funktionen übergeben werden, die nicht generisch sein sollten (wenn Sie einen verwenden, der objectmöglicherweise übertrieben oder langsam ist, wenn Sie einen Werttyp benötigen), wenn Sie einen undurchsichtigen Zeiger benötigen, oder für den Fall, dass Sie schnell auf ein Element eines Arrays zugreifen müssen, dessen Spezifikationen Sie zur Laufzeit finden (mit Array.InternalGetReference). Warum wird davon abgeraten, da die CLR nicht einmal eine falsche Verwendung dieses Typs zulässt? Es scheint nicht unsicher zu sein oder so ...


Andere Verwendungen, die ich gefunden habe für TypedReference:

"Spezialisierung" von Generika in C # (dies ist typsicher):

static void foo<T>(ref T value)
{
    //This is the ONLY way to treat value as int, without boxing/unboxing objects
    if (value is int)
    { __refvalue(__makeref(value), int) = 1; }
    else { value = default(T); }
}

Schreiben von Code, der mit generischen Zeigern funktioniert (dies ist sehr unsicher, wenn es missbraucht wird, aber schnell und sicher, wenn es richtig verwendet wird):

//This bypasses the restriction that you can't have a pointer to T,
//letting you write very high-performance generic code.
//It's dangerous if you don't know what you're doing, but very worth if you do.
static T Read<T>(IntPtr address)
{
    var obj = default(T);
    var tr = __makeref(obj);

    //This is equivalent to shooting yourself in the foot
    //but it's the only high-perf solution in some cases
    //it sets the first field of the TypedReference (which is a pointer)
    //to the address you give it, then it dereferences the value.
    //Better be 10000% sure that your type T is unmanaged/blittable...
    unsafe { *(IntPtr*)(&tr) = address; }

    return __refvalue(tr, T);
}

Schreiben einer Methodenversion der sizeofAnweisung, die gelegentlich nützlich sein kann:

static class ArrayOfTwoElements<T> { static readonly Value = new T[2]; }

static uint SizeOf<T>()
{
    unsafe 
    {
        TypedReference
            elem1 = __makeref(ArrayOfTwoElements<T>.Value[0] ),
            elem2 = __makeref(ArrayOfTwoElements<T>.Value[1] );
        unsafe
        { return (uint)((byte*)*(IntPtr*)(&elem2) - (byte*)*(IntPtr*)(&elem1)); }
    }
}

Schreiben einer Methode, die einen "state" -Parameter übergibt, der das Boxen vermeiden möchte:

static void call(Action<int, TypedReference> action, TypedReference state)
{
    //Note: I could've said "object" instead of "TypedReference",
    //but if I had, then the user would've had to box any value types
    try
    {
        action(0, state);
    }
    finally { /*Do any cleanup needed*/ }
}

Warum werden solche Verwendungen "entmutigt" (mangels Dokumentation)? Irgendwelche besonderen Sicherheitsgründe? Es scheint vollkommen sicher und überprüfbar zu sein, wenn es nicht mit Zeigern gemischt ist (die sowieso nicht sicher oder überprüfbar sind) ...


Aktualisieren:

Beispielcode, der zeigt, dass er tatsächlich TypedReferencedoppelt so schnell (oder mehr) sein kann:

using System;
using System.Collections.Generic;
static class Program
{
    static void Set1<T>(T[] a, int i, int v)
    { __refvalue(__makeref(a[i]), int) = v; }

    static void Set2<T>(T[] a, int i, int v)
    { a[i] = (T)(object)v; }

    static void Main(string[] args)
    {
        var root = new List<object>();
        var rand = new Random();
        for (int i = 0; i < 1024; i++)
        { root.Add(new byte[rand.Next(1024 * 64)]); }
        //The above code is to put just a bit of pressure on the GC

        var arr = new int[5];
        int start;
        const int COUNT = 40000000;

        start = Environment.TickCount;
        for (int i = 0; i < COUNT; i++)
        { Set1(arr, 0, i); }
        Console.WriteLine("Using TypedReference:  {0} ticks",
                          Environment.TickCount - start);
        start = Environment.TickCount;
        for (int i = 0; i < COUNT; i++)
        { Set2(arr, 0, i); }
        Console.WriteLine("Using boxing/unboxing: {0} ticks",
                          Environment.TickCount - start);

        //Output Using TypedReference:  156 ticks
        //Output Using boxing/unboxing: 484 ticks
    }
}

(Bearbeiten: Ich habe den obigen Benchmark bearbeitet, da in der letzten Version des Beitrags eine Debug-Version des Codes verwendet wurde [ich habe vergessen, ihn in Release zu ändern] und keinen Druck auf den GC ausgeübt hat. Diese Version ist etwas realistischer und auf meinem System ist es TypedReferenceim Durchschnitt mehr als dreimal schneller .)

user541686
quelle
Wenn ich Ihr Beispiel ausführe, erhalte ich völlig andere Ergebnisse. TypedReference: 203 ticks, boxing/unboxing: 31 ticks. Egal was ich versuche (einschließlich verschiedener Möglichkeiten, das Timing durchzuführen), das Boxen / Unboxen ist auf meinem System immer noch schneller.
Seph
1
@Seph: Ich habe gerade deinen Kommentar gesehen. Das ist sehr interessant - es scheint auf x64 schneller zu sein, aber auf x86 langsamer.
Seltsam
1
Ich habe diesen Benchmark-Code gerade auf meinem x64-Computer unter .NET 4.5 getestet. Ich habe Environment.TickCount durch Diagnostics.Stopwatch ersetzt und mich für ms anstelle von Ticks entschieden. Ich habe jeden Build (x86, 64, Any) dreimal ausgeführt. Das beste von drei Ergebnissen war wie folgt: x86: 205/27 ms (gleiches Ergebnis für 2/3 Läufe in diesem Build) x64: 218/109 ms Beliebig: 205/27 ms (gleiches Ergebnis für 2/3 Läufe in diesem Build) -all- Fälle Box / Unboxing war schneller.
kornman00
2
Die seltsamen Geschwindigkeitsmessungen könnten auf diese beiden Tatsachen zurückgeführt werden: * (T) (Objekt) v führt KEINE Heap-Zuordnung durch. In .NET 4+ ist es weg optimiert. Es gibt keine Zuweisungen auf diesem Weg und es ist verdammt schnell. * Für die Verwendung von makeref muss die Variable tatsächlich auf dem Stapel zugeordnet sein (während die Arta-Box-Methode sie möglicherweise in Registern optimiert). Wenn ich mir die Timings anschaue, gehe ich auch davon aus, dass dies das Inlining selbst mit der Force-Inline-Flagge beeinträchtigt. So kinda-Box ist inline und registriert, während makeref einen Funktionsaufruf macht und den Stack betreibt
hypersw
1
Machen Sie es weniger trivial, um die Gewinne des Typeref-Castings zu sehen. ZB einen zugrunde liegenden Typ in den Aufzählungstyp ( int-> DockStyle) umwandeln . Diese Box ist echt und fast zehnmal langsamer.
Hypersw

Antworten:

42

Kurze Antwort: Portabilität .

Während __arglist, __makerefund __refvaluesind Spracherweiterungen und sind in der C # Language Specification ohne Papiere, die verwendeten Konstrukte sie unter der Haube zu implementieren ( varargAufrufkonvention, TypedReferenceArt, arglist, refanytype, mkanyref, und refanyvalAnweisungen) sind perfekt in der dokumentierten CLI - Spezifikation (ECMA-335) in die Vararg Bibliothek .

Die Definition in der Vararg-Bibliothek macht deutlich, dass sie in erster Linie Argumentlisten mit variabler Länge unterstützen sollen und nicht viel mehr. Listen mit variablen Argumenten sind auf Plattformen, die keine Schnittstelle zu externem C-Code mit varargs verwenden müssen, wenig nützlich. Aus diesem Grund ist die Varargs-Bibliothek nicht Teil eines CLI-Profils. In legitimen CLI-Implementierungen wird die Varargs-Bibliothek möglicherweise nicht unterstützt, da sie nicht im CLI-Kernel-Profil enthalten ist:

4.1.6 Vararg

Der vararg-Funktionsumfang unterstützt Argumentlisten mit variabler Länge und zur Laufzeit typisierte Zeiger.

Wenn nicht angegeben: Jeder Versuch, eine Methode mit der varargaufrufenden Konvention oder den mit vararg-Methoden verknüpften Signaturcodierungen zu referenzieren (siehe Partition II), löst die System.NotImplementedExceptionAusnahme aus. Methoden , um die CIL Anweisungen mit arglist, refanytype, mkrefany, und refanyvalwerden die werfen System.NotImplementedExceptionAusnahme. Der genaue Zeitpunkt der Ausnahme ist nicht angegeben. Der Typ System.TypedReferencemuss nicht definiert werden.

Update (Antwort auf GetValueDirectKommentar):

FieldInfo.GetValueDirectsind FieldInfo.SetValueDirectsind nicht von Basisklassenbibliothek Teil. Beachten Sie, dass es einen Unterschied zwischen der .NET Framework-Klassenbibliothek und der Basisklassenbibliothek gibt. BCL ist das einzige, was für eine konforme Implementierung der CLI / C # erforderlich ist, und ist in ECMA TR / 84 dokumentiert . (Tatsächlich ist es FieldInfoselbst Teil der Reflection-Bibliothek und auch nicht im CLI-Kernel-Profil enthalten.)

Sobald Sie eine Methode außerhalb von BCL verwenden, geben Sie ein wenig Portabilität auf (und dies wird mit dem Aufkommen von Nicht-.NET-CLI-Implementierungen wie Silverlight und MonoTouch immer wichtiger). Selbst wenn eine Implementierung die Kompatibilität mit der Microsoft .NET Framework-Klassenbibliothek verbessern wollte, konnte sie einfach eine bereitstellen GetValueDirectund SetValueDirectausführen, TypedReferenceohne dass TypedReferencedie Laufzeit speziell dafür behandelt wird (im Grunde genommen sind sie ihren objectGegenstücken ohne Leistungsvorteil gleichwertig ).

Hätten sie es in C # dokumentiert, hätte es zumindest ein paar Auswirkungen gehabt:

  1. Wie jedes Merkmal, es kann ein Hindernis für neue Funktionen, vor allem , da dies eine nicht wirklich bei der Gestaltung von C # paßt und erfordert seltsame Syntax - Erweiterungen und Sondergabe eines Typs von der Laufzeit.
  2. Alle Implementierungen von C # müssen diese Funktion irgendwie implementieren, und es ist nicht unbedingt trivial / möglich für C # -Implementierungen, die überhaupt nicht auf einer CLI oder ohne Varargs auf einer CLI ausgeführt werden.
Mehrdad Afshari
quelle
4
Gute Argumente für Portabilität, +1. Aber was ist mit FieldInfo.GetValueDirectund FieldInfo.SetValueDirect? Sie sind Teil der BCL, und um sie zu verwenden, müssen Sie sie verwenden. Muss TypedReference das nicht grundsätzlich TypedReferenceimmer definiert werden, unabhängig von der Sprachspezifikation? (Noch ein Hinweis: Auch wenn die Schlüsselwörter nicht vorhanden waren, konnten Sie, solange die Anweisungen vorhanden waren, durch dynamisches Ausgeben von Methoden darauf zugreifen. Solange Ihre Plattform mit C-Bibliotheken zusammenarbeitet, können Sie diese verwenden. ob C # die Schlüsselwörter hat oder nicht.)
user541686
Oh, und noch ein Problem: Auch wenn es nicht portabel ist, warum haben sie die Schlüsselwörter nicht dokumentiert? Zumindest ist es notwendig, wenn man mit C-Varargs interagiert, also hätten sie es zumindest erwähnen können?
user541686
@Mehrdad: Huh, das ist interessant. Ich habe wohl immer angenommen, dass Dateien im BCL- Ordner der .NET-Quelle Teil der BCL sind, ohne wirklich auf den ECMA-Standardisierungsteil zu achten. Das ist ziemlich überzeugend ... bis auf eine Kleinigkeit: Ist es nicht ein bisschen sinnlos, die (optionale) Funktion in die CLI-Spezifikation aufzunehmen, wenn es keine Dokumentation zur Verwendung gibt? (Es wäre sinnvoll, wenn TypedReferencenur eine Sprache dokumentiert wäre - beispielsweise verwaltetes C ++ -, aber wenn keine Sprache dies dokumentiert und wenn niemand es wirklich verwenden kann, warum sollte man sich dann überhaupt die Mühe machen, die Funktion zu definieren?)
user541686
@Mehrdad Ich vermute, die Hauptmotivation war die Notwendigkeit dieser Funktion intern für Interop ( z. B. [DllImport("...")] void Foo(__arglist); ) und sie haben sie in C # für ihren eigenen Gebrauch implementiert. Das Design der CLI wird von vielen Sprachen beeinflusst (Anmerkungen "The Common Language Infrastructure Annotated Standard" belegen diese Tatsache). Eine geeignete Laufzeit für so viele Sprachen wie möglich, einschließlich unvorhergesehener, zu sein, war definitiv ein Designziel (daher das name) und dies ist eine Funktion, von der beispielsweise eine hypothetisch verwaltete C-Implementierung wahrscheinlich profitieren könnte.
Mehrdad Afshari
@Mehrdad: Ah ... ja, das ist ein ziemlich überzeugender Grund. Vielen Dank!
user541686
15

Nun, ich bin kein Eric Lippert, daher kann ich nicht direkt über die Motivationen von Microsoft sprechen, aber wenn ich eine Vermutung wagen würde, würde ich sagen, dass TypedReferenceet al. sind nicht gut dokumentiert, weil Sie sie ehrlich gesagt nicht brauchen.

Jede Verwendung, die Sie für diese Funktionen erwähnt haben, kann ohne sie ausgeführt werden, wenn auch in einigen Fällen mit einem Leistungsverlust. C # (und .NET im Allgemeinen) ist jedoch nicht als Hochleistungssprache konzipiert. (Ich vermute, dass "schneller als Java" das Leistungsziel war.)

Das heißt nicht, dass bestimmte Leistungsaspekte nicht berücksichtigt wurden. In der Tat existieren Funktionen wie Zeiger stackallocund bestimmte optimierte Framework-Funktionen weitgehend, um die Leistung in bestimmten Situationen zu steigern.

Generics, die ich würde sagen , die haben primäre Nutzen der Typsicherheit, verbessert auch die Leistung ähnlich wie TypedReferencedurch Box und Unboxing zu vermeiden. Tatsächlich habe ich mich gefragt, warum Sie dies bevorzugen würden:

static void call(Action<int, TypedReference> action, TypedReference state){
    action(0, state);
}

dazu:

static void call<T>(Action<int, T> action, T state){
    action(0, state);
}

Die Kompromisse sind meines Erachtens, dass erstere weniger JITs (und folglich weniger Speicher) benötigen, während letztere vertrauter und, wie ich annehmen würde, etwas schneller sind (durch Vermeidung von Zeiger-Dereferenzierungen).

Ich würde anrufen TypedReferenceund Freunde Implementierungsdetails. Sie haben auf einige nützliche Verwendungszwecke hingewiesen, und ich denke, sie sind es wert, untersucht zu werden, aber die übliche Einschränkung, sich auf Implementierungsdetails zu verlassen, gilt - die nächste Version kann Ihren Code beschädigen.

P Papa
quelle
4
Huh ... "du brauchst sie nicht" - ich hätte das kommen sehen sollen. :-) Das stimmt, aber es stimmt auch nicht. Was definieren Sie als "Bedürfnis"? Werden beispielsweise Erweiterungsmethoden wirklich "benötigt"? In Bezug auf Ihre Frage zur Verwendung von Generika in call(): Es liegt daran, dass der Code nicht immer so zusammenhängend ist - ich bezog mich eher auf ein Beispiel wie das von IAsyncResult.State, bei dem die Einführung von Generika einfach nicht durchführbar wäre, weil plötzlich Generika für eingeführt würden jede Klasse / Methode beteiligt. +1 für die Antwort ... vor allem, um auf den Teil "schneller als Java" hinzuweisen. :]
user541686
1
Oh, und noch ein Punkt: Es TypedReferencewird wahrscheinlich bald keine bahnbrechenden Änderungen mehr geben, da FieldInfo.SetValueDirect , das öffentlich ist und wahrscheinlich von einigen Entwicklern verwendet wird, davon abhängt. :)
user541686
Ah, aber Sie haben Methoden benötigen Erweiterung, um Unterstützung LINQ. Wie auch immer, ich spreche nicht wirklich von einem Unterschied, den man haben muss / haben muss. Ich würde keines von TypedReferencebeiden anrufen . (Die grausame Syntax und die allgemeine Unhandlichkeit disqualifizieren es meiner Meinung nach von der Kategorie "schön zu haben".) Ich würde sagen, es ist nur eine gute Sache, wenn man hier und da wirklich ein paar Mikrosekunden kürzen muss. Trotzdem denke ich an einige Stellen in meinem eigenen Code, die ich mir jetzt ansehen werde, um zu sehen, ob ich sie mit den von Ihnen genannten Techniken optimieren kann.
P Daddy
1
@Merhdad: Ich habe zu der Zeit an einem Serializer / Deserializer für binäre Objekte für die Interprozess- / Interhost-Kommunikation (TCP und Pipes) gearbeitet. Mein Ziel war es, es so klein (in Bezug auf über das Kabel gesendete Bytes) und schnell (in Bezug auf die Zeit, die für das Serialisieren und Deserialisieren aufgewendet wurde) wie möglich zu gestalten. Ich dachte, ich könnte das Boxen und Unboxen mit TypedReferences vermeiden , aber IIRC, der einzige Ort, an dem ich das Boxen irgendwo vermeiden konnte, waren die Elemente eindimensionaler Anordnungen von Grundelementen. Der leichte Geschwindigkeitsvorteil hier war die Komplexität, die er dem gesamten Projekt hinzufügte, nicht wert, also habe ich ihn herausgenommen.
P Daddy
1
Bei delegate void ActByRef<T1,T2>(ref T1 p1, ref T2 p2);einer gegebenen Typensammlung Tkönnte eine Methode ActOnItem<TParam>(int index, ActByRef<T,TParam> proc, ref TParam param)bereitgestellt werden , aber der JITter müsste für jeden Werttyp eine andere Version der Methode erstellen TParam. Die Verwendung einer typisierten Referenz würde es einer JITted-Version der Methode ermöglichen, mit allen Parametertypen zu arbeiten.
Supercat
4

Ich kann nicht herausfinden, ob der Titel dieser Frage sarkastisch sein soll: Es ist seit langem bekannt, dass TypedReferencees sich um den langsamen, aufgeblähten, hässlichen Cousin von "echten" verwalteten Zeigern handelt. Letzteres ist das, was wir mit C ++ / CLI erhalten interior_ptr<T> , oder sogar traditionelle Referenzparameter ( ref/ out) in C # . Tatsächlich ist es ziemlich schwierig, die TypedReferenceBasisleistung zu erreichen , wenn nur eine Ganzzahl verwendet wird, um das ursprüngliche CLR-Array jedes Mal neu zu indizieren.

Die traurigen Details sind hier , aber zum Glück ist nichts davon jetzt wichtig ...

Diese Frage wird jetzt von den neuen Ref-Einheimischen und Ref-Return- Funktionen in C # 7 zur Diskussion gestellt

Diese neuen Sprachfunktionen bieten in C # eine erstklassige Unterstützung für das Deklarieren, Freigeben und Bearbeiten von echten CLR verwalteten Referenztyp- Typen in sorgfältig vorgegebenen Situationen.

Die Nutzungsbeschränkungen sind nicht strenger als das, was bisher erforderlich für TypedReference(und die Leistung ist buchstäblich einen Sprung von Schlimmste am besten ), so sehe ich keinen verbleibenden denkbar Anwendungsfall in C # für TypedReference. Bisher gab es beispielsweise keine Möglichkeit, ein TypedReferenceim GCHeap beizubehalten, sodass das Gleiche für die überlegenen verwalteten Zeiger jetzt kein Take-Away mehr ist.

Und natürlich bedeutet der Niedergang von TypedReference- oder zumindest seine fast vollständige Abwertung - auch, __makerefden Müll zu werfen .

Glenn Slayden
quelle