Aufschlüsselung der Grundlagen von C #: Zuweisen von Speicher für einen Referenztyp auf dem Stack

    In diesem Artikel werden die Grundlagen interner Gerätetypen sowie ein Beispiel beschrieben, in dem der Speicher für den Referenztyp vollständig auf dem Stapel reserviert wird (weil ich ein Full-Stack-Programmierer bin).



    Haftungsausschluss


    Dieser Artikel enthält kein Material, das in realen Projekten verwendet werden sollte. Es ist einfach eine Erweiterung der Grenzen, in denen eine Programmiersprache wahrgenommen wird.

    Bevor Sie mit der Geschichte fortfahren , empfehle ich Ihnen dringend , den ersten Beitrag über StructLayout zu lesen , weil Es gibt ein Beispiel, das in diesem Artikel verwendet wird (jedoch wie immer).

    Vorgeschichte


    Als ich anfing, Code für diesen Artikel zu schreiben, wollte ich mit Assembler etwas Interessantes tun. Ich wollte das Standard-Ausführungsmodell irgendwie brechen und ein wirklich ungewöhnliches Ergebnis erzielen. Ich erinnere mich an die Häufigkeit, mit der die Leute sagen, dass der Referenztyp sich von dem signifikanten unterscheidet, indem sich der erste auf dem Haufen und der zweite auf dem Stapel befindet. Ich entschied mich für einen Assembler, um zu zeigen, dass der Referenztyp auf dem Stapel leben kann. Ich fing jedoch an, auf alle möglichen Probleme zu stoßen, zum Beispiel, die gewünschte Adresse und ihre Präsentation als verwalteten Link zurückzugeben (ich arbeite immer noch daran). Also fing ich an zu schummeln und etwas zu tun, was in Assembler nicht funktioniert, in C #. Und am Ende bleibt der Assembler überhaupt nicht übrig.
    Es wird auch empfohlen zu lesen - wenn Sie mit dem Gerät von Referenztypen vertraut sind, empfehle ich, die Theorie darüber zu überspringen (nur die Grundlagen werden gegeben, nichts Interessantes).

    Ein wenig über die internen Gerätetypen


    Ich möchte daran erinnern, dass die Aufteilung des Speichers in einen Stack und einen Heap auf .NET-Ebene erfolgt und diese Aufteilung ist rein logisch: Es gibt keinen physischen Unterschied zwischen den Speicherbereichen unter dem Heap und dem Stack. Der Unterschied in der Produktivität wird bereits speziell für die Arbeit mit diesen Bereichen angegeben.

    Wie kann man dann Speicher auf dem Stack reservieren? Lassen Sie uns zunächst verstehen, wie dieser geheimnisvolle Referenztyp angeordnet ist und was darin ist, was nicht im Sinn ist.

    Betrachten wir also das einfachste Beispiel mit der Klasse Employee.

    Mitarbeitercode
    publicclassEmployee 
    {
        privateint _id;
        privatestring _name;
        publicvirtualvoidWork() 
        {        
                Console.WriteLine(“Zzzz...”);
        }
        publicvoidTakeVacation(int days) 
        {
                Console.WriteLine(“Zzzz...”);
        }
        publicstaticvoidSetCompanyPolicy(CompanyPolicy policy)
        {       
                 Console.WriteLine("Zzzz...");
        }
     } 
    


    Und schauen Sie sich an, wie es im Speicher dargestellt wird.
    UPD: Diese Klasse wird am Beispiel eines 32-Bit-Systems betrachtet.



    Zusätzlich zum Speicher für die Felder haben wir also zwei weitere verborgene Felder - den Index des Synchronisationsblocks (das Wort des Objekttitels im Bild) und die Adresse der Methodentabelle.

    Das erste Feld, es ist der Synchronisationsblockindex, interessiert uns nicht wirklich. Als ich den Typ platzierte, beschloss ich, ihn zu senken. Ich habe das aus zwei Gründen getan:

    1. Ich bin sehr faul (ich habe nicht gesagt, dass die Gründe vernünftig sind)
    2. Für die Grundoperation des Objekts ist dieses Feld nicht erforderlich.

    Aber da wir bereits angefangen haben zu reden, halte ich es für richtig, einige Worte zu diesem Bereich zu sagen. Es wird für verschiedene Zwecke (Hash-Code, Synchronisation) verwendet. Vielmehr ist das Feld selbst einfach ein Index eines der Synchronisationsblöcke, die dem gegebenen Objekt zugeordnet sind. Die Blöcke selbst befinden sich in der Tabelle der Synchronisationsblöcke (ein globales Array). Das Erstellen eines solchen Blocks ist eine ziemlich große Operation, daher wird er nicht erstellt, wenn er nicht benötigt wird. Wenn Sie dünne Sperren verwenden, wird außerdem der Bezeichner des Threads, der die Sperre erhalten hat (anstelle des Index), dorthin geschrieben.

    Das zweite Feld ist für uns viel wichtiger. Dank der Tabelle der Typmethoden ist ein so mächtiges Werkzeug wie Polymorphismus möglich (das Strukturen, Stack-Könige übrigens nicht besitzen). Angenommen, die Employee-Klasse implementiert zusätzlich drei Schnittstellen: IComparable, IDisposable und ICloneable.

    Die Methodentabelle sieht dann etwa so aus: Das



    Bild ist sehr cool, im Prinzip ist alles dort gemalt und verständlich. Wenn es kurz ist, wird die virtuelle Methode nicht direkt über die Adresse aufgerufen, sondern durch den Versatz in der Methodentabelle. In der Hierarchie befinden sich dieselben virtuellen Methoden in der Methodentabelle in derselben Versetzung. Das heißt, in der Basisklasse rufen wir die Methode mit dem Offset auf, ohne zu wissen, welcher Typ von Methodentabelle verwendet wird, aber zu wissen, dass dieser Offset die relevanteste Methode für den Laufzeittyp ist.

    Es ist auch zu beachten, dass die Objektreferenz auf die Methodentabelle verweist.

    Lang erwartete Beispiel


    Beginnen wir mit einem Unterricht, der uns bei unserem Ziel helfen wird. Mit StructLayout (ich habe es wirklich ausprobiert, aber es hat nicht geklappt), schrieb ich die einfachsten Mapper-Zeiger auf verwaltete Typen und zurück. Einen Zeiger von einem verwalteten Link zu bekommen, ist ziemlich einfach, aber die inverse Transformation bereitete mir Schwierigkeiten und ohne zu überlegen, habe ich mein Lieblingsattribut angewendet. Um den Code in einer Taste zu halten, hergestellt in zwei Richtungen auf eine Weise.

    Code hier
    // Предоставляет нужные нам сигнатурыpublicclassPointerCasterFacade 
    {
        publicvirtualunsafe T GetManagedReferenceByPointer<T>(int* pointer) => default(T);
        publicvirtualunsafeint* GetPointerByManagedReference<T>(T managedReference) => (int*)0;
    }
    // Предоставляет нужную нам логикуpublicclassPointerCasterUnderground
    {
        publicvirtual T GetManagedReferenceByPointer<T>(T reference) => reference;
        publicvirtualunsafeint* GetPointerByManagedReference<T>(int* pointer) => pointer;
    }
    [StructLayout(LayoutKind.Explicit)]
    publicclassPointerCaster
    {
        publicPointerCaster()
        {
            pointerCaster= new PointerCasterUnderground();
        }
        [FieldOffset(0)]
        private PointerCasterUnderground pointerCaster;
        [FieldOffset(0)]
        public PointerCasterFacade Caster;
    }
    


    Zuerst schreiben wir eine Methode, die einen Zeiger auf einen Speicher (nicht unbedingt auf den Stapel) nimmt und den Typ konfiguriert.

    Um die Adresse der Methodentabelle einfach zu finden, erstelle ich einen Typ auf dem Heap. Ich bin mir sicher, dass die Methodentabelle auf andere Weise gefunden werden kann, aber ich habe mir nicht das Ziel gesetzt, diesen Code zu optimieren. Mit den zuvor beschriebenen Konvertern erhalten wir einen Zeiger auf den erstellten Typ.

    Dieser Zeiger zeigt genau auf die Methodentabelle. Daher reicht es aus, den Inhalt einfach aus dem Speicher zu erhalten, auf den er verweist. Dies ist die Adresse der Methodentabelle.
    Und da der an uns übergebene Zeiger eine Art Objektreferenz ist, müssen wir auch die Adresse der Methodentabelle genau dorthin schreiben, wo sie zeigt.

    Das ist eigentlich alles. Plötzlich richtig? Nun ist unser Typ fertig. Pinocchio, der uns das Gedächtnis zugewiesen hat, wird sich um die Initialisierung der Felder kümmern.

    Es bleibt nur noch Grandkasterom zu verwenden, um den Zeiger in einen verwalteten Link zu konvertieren.

    publicclassStackInitializer
    {
        publicstaticunsafe T InitializeOnStack<T>(int* pointer) where T : new()
        {
            T r = new T();
            var caster = new PointerCaster().Caster;
            int* ptr = caster.GetPointerByManagedReference(r);
            pointer[0] = ptr[0];
            T reference = caster.GetManagedReferenceByPointer<T>(pointer);
            return reference;
        }
    }
    

    Nun haben wir einen Link auf dem Stack, der auf den gleichen Stack zeigt, wo nach allen Gesetzen der Referenztypen (na ja, fast) ein aus schwarzen Erde und Stöcken konstruiertes Objekt liegt. Polymorphismus ist verfügbar.

    Es versteht sich, dass wir, wenn Sie diesen Link außerhalb der Methode übergeben, nach der Rückkehr von der Methode etwas Unklares bekommen. Über Aufrufe von virtuellen Methoden und Sprache kann es nicht gehen, wir fliegen über die Ausnahme. Normale Methoden werden direkt aufgerufen, der Code enthält nur Adressen für echte Methoden, damit sie funktionieren. Und anstelle der Felder wird es sein ... und niemand weiß, was da sein wird.

    Da es nicht möglich ist, eine separate Methode für die Initialisierung auf dem Stack zu verwenden (da der Stack-Frame nach der Rückkehr von der Methode überschrieben wird), muss die Methode, die den Typ auf den Stack anwenden möchte, Speicher zuordnen. Streng genommen gibt es keine Möglichkeit, dies zu tun. Am besten geeignet für uns ist stackalloc. Genau das richtige Keyword für unsere Zwecke. Unglücklicherweise verursachte dies die Unkontrollierbarkeit im Code. Zuvor gab es die Idee, Span für diesen Zweck zu verwenden und auf unsicheren Code zu verzichten. Im unsicheren Code gibt es nichts Schlechtes, aber wie überall ist es keine Wunderwaffe und hat seine eigenen Anwendungsbereiche.

    Nachdem wir den Zeiger auf den Speicher im aktuellen Stapel erhalten haben, übergeben wir diesen Zeiger an die Methode, die den Typ in Teilen ausmacht. Das ist alles, was zugehört hat - gut gemacht.

    unsafeclassProgram
    {
        publicstaticvoidMain()
        {
            int* pointer = stackallocint[2];
            var a = StackInitializer.InitializeOnStack<StackReferenceType>(pointer);
            a.StubMethod();
            Console.WriteLine(a.Field);
            Console.WriteLine(a);
            Console.Read();
        }
    }
    

    Sie sollten es nicht in realen Projekten verwenden, die Methode, die Speicherplatz auf dem Stack zuweist, verwendet neues T (), das wiederum mithilfe von Reflektion einen Typ auf dem Heap erstellt. Diese Methode wird also langsamer sein als bei der üblichen Erstellung der Art von Zeiten, gut in 40-50.

    Hier können Sie das gesamte Projekt sehen.

    Quelle: Im theoretischen Handbuch wurden Beispiele aus dem Buch Sasha Goldstein - Pro .NET Performace verwendet

    Jetzt auch beliebt: