Grundlegendes zu C #: Zuweisen von Speicher für einen Referenztyp auf dem Stapel

In diesem Artikel werden die Grundlagen des internen Gerätetyps sowie ein Beispiel gegeben, in dem der Speicher für den Referenztyp vollständig auf dem Stapel zugewiesen wird (dies liegt daran, dass 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 beginnen, empfehle ich dringend, dass Sie den ersten Beitrag über StructLayout lesen , weil Dort wird ein Beispiel analysiert, das in diesem Artikel verwendet wird (jedoch wie immer).

Hintergrund


Als ich anfing, den Code für diesen Artikel zu schreiben, wollte ich mit der Assemblersprache etwas Interessantes machen. Ich wollte irgendwie das Standard-Ausführungsmodell brechen und ein wirklich ungewöhnliches Ergebnis erzielen. Und als ich mich daran erinnerte, wie oft Leute sagen, dass sich der Referenztyp von dem signifikanten darin unterscheidet, dass sich der erste auf dem Heap und der zweite auf dem Stapel befindet, entschied ich mich, Assembler zu verwenden, um zu zeigen, dass der Referenztyp auf dem Stapel leben kann. Es traten jedoch alle möglichen Probleme auf, z. B. die Rückgabe der gewünschten Adresse und die Darstellung als verwalteter Link (ich arbeite noch daran). Also fing ich an zu tricksen und zu tun, was in Assembler in C # nicht funktioniert. Und am Ende blieb der Assembler überhaupt nicht.
Auch eine Empfehlung zum Lesen - wenn Sie mit dem Gerät der Referenztypen vertraut sind, empfehle ich, dass Sie die Theorie über sie überspringen (nur die Grundlagen werden gegeben, nichts Interessantes).

Ein bisschen über die interne Struktur von Typen


Ich möchte Sie daran erinnern, dass die Trennung von Speicher auf dem Stapel und dem Heap auf .NET-Ebene erfolgt und diese Aufteilung rein logisch ist. Physikalisch gibt es keinen Unterschied zwischen den Speicherbereichen unter dem Heap und unter dem Stapel. Der Produktivitätsunterschied wird bereits durch die Arbeit mit diesen Bereichen deutlich.

Wie ordne ich dann Speicher auf dem Stapel zu? Lassen Sie uns zunächst sehen, wie dieser mysteriöse Referenztyp strukturiert ist und was darin enthalten ist, was nicht signifikant ist.

Betrachten Sie also das einfachste Beispiel mit der Employee-Klasse.

Mitarbeitercode
public class Employee { private int _id; private string _name; public virtual void Work() { Console.WriteLine(“Zzzz...”); } public void TakeVacation(int days) { Console.WriteLine(“Zzzz...”); } public static void SetCompanyPolicy(CompanyPolicy policy) { Console.WriteLine("Zzzz..."); } } 


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



Somit haben wir zusätzlich zum Speicher für die Felder zwei weitere versteckte Felder - den Index des Synchronisationsblocks (das Titelwort des Objekts im Bild) und die Adresse der Methodentabelle.

Das erste Feld, es ist der Index des Synchronisationsblocks, interessiert uns nicht besonders. Beim Platzieren des Typs habe ich beschlossen, ihn wegzulassen. Ich habe das aus zwei Gründen getan:

  1. Ich bin sehr faul (ich habe nicht gesagt, dass die Gründe vernünftig sein werden)
  2. Dieses Feld ist für die Grundfunktion des Objekts optional.

Aber da wir bereits gesprochen haben, halte ich es für richtig, ein paar Worte zu diesem Bereich zu sagen. Es wird für verschiedene Zwecke verwendet (Hash-Code, Synchronisation). Das Feld selbst ist vielmehr einfach der Index eines der diesem Objekt zugeordneten Synchronisationsblöcke. Die Blöcke selbst befinden sich in der Tabelle der Synchronisationsblöcke (a la global 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 dort außerdem die Kennung des Threads geschrieben, der die Sperre erhalten hat (anstelle des Index).

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 übrigens nicht von der Struktur, den Königen des Stapels, besessen wird). Angenommen, die Employee-Klasse implementiert zusätzlich drei Schnittstellen: IComparable, IDisposable und ICloneable.

Dann sieht die Methodentabelle ungefähr so ​​aus



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

Es ist auch zu beachten, dass der Verweis auf das Objekt auf die Methodentabelle verweist.

Das lang erwartete Beispiel


Beginnen wir mit Kursen, die uns bei unserem Ziel helfen. Mit StructLayout (ich habe es wirklich ohne versucht, aber es hat nicht geklappt) habe ich die einfachsten Zeiger-Mapper für verwaltete Typen geschrieben und umgekehrt. Es ist ziemlich einfach, einen Zeiger von einem verwalteten Link zu erhalten, aber die inverse Transformation hat mir Schwierigkeiten bereitet, und ohne nachzudenken, habe ich mein Lieblingsattribut angewendet. Um den Code in einem Schlüssel zu halten, habe ich ihn auf eine Weise in zwei Richtungen ausgeführt.

Code hier
 //     public class PointerCasterFacade { public virtual unsafe T GetManagedReferenceByPointer<T>(int* pointer) => default(T); public virtual unsafe int* GetPointerByManagedReference<T>(T managedReference) => (int*)0; } //     public class PointerCasterUnderground { public virtual T GetManagedReferenceByPointer<T>(T reference) => reference; public virtual unsafe int* GetPointerByManagedReference<T>(int* pointer) => pointer; } [StructLayout(LayoutKind.Explicit)] public class PointerCaster { public PointerCaster() { pointerCaster= new PointerCasterUnderground(); } [FieldOffset(0)] private PointerCasterUnderground pointerCaster; [FieldOffset(0)] public PointerCasterFacade Caster; } 


Schreiben Sie zunächst eine Methode, die einen Zeiger auf einen Speicher (übrigens nicht unbedingt auf dem Stapel) verwendet und den Typ konfiguriert.

Um das Auffinden der Adresse der Methodentabelle zu erleichtern, erstelle ich einen Typ auf dem Heap. Ich bin sicher, dass die Methodentabelle auf andere Weise gefunden werden kann, aber ich habe mir nicht das Ziel gesetzt, diesen Code zu optimieren. Es war für mich interessanter, ihn verständlich zu machen. Als nächstes erhalten wir unter Verwendung der zuvor beschriebenen Konverter einen Zeiger auf den erstellten Typ.

Dieser Zeiger zeigt genau auf die Methodentabelle. Daher reicht es aus, den Inhalt einfach aus dem Speicher abzurufen, auf den er verweist. Dies ist die Adresse der Methodentabelle.
Und da der an uns übergebene Zeiger eine Art Verweis auf das Objekt ist, müssen wir die Adresse der Methodentabelle genau dort notieren, wo sie zeigt.

Das ist eigentlich alles. Unerwartet, richtig? Jetzt ist unser Typ fertig. Pinocchio, der uns den Speicher zugewiesen hat, kümmert sich um die Initialisierung der Felder.

Es bleibt nur die Verwendung des Grandcasters, um den Zeiger in einen verwalteten Link umzuwandeln.

 public class StackInitializer { public static unsafe 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; } } 

Jetzt haben wir einen Link auf dem Stapel, der auf denselben Stapel verweist, in dem nach allen Gesetzen der Referenztypen (na ja, fast) ein Objekt aus schwarzem Boden und Stöcken liegt. Polymorphismus ist verfügbar.

Es versteht sich, dass wir, wenn Sie diesen Link außerhalb der Methode übergeben, nach der Rückkehr etwas Unklares erhalten. Es kann nicht von Aufrufen virtueller Methoden gesprochen werden. Lassen Sie uns ausnahmsweise fliegen. Reguläre Methoden werden direkt aufgerufen, im Code gibt es einfach Adressen für echte Methoden, damit sie funktionieren. Und an der Stelle der Felder wird ... aber niemand weiß, was da sein wird.

Da es unmöglich ist, eine separate Methode für die Initialisierung auf dem Stapel zu verwenden (da der Stapelrahmen nach der Rückkehr von der Methode gelöscht wird), sollte der Speicher von der Methode zugewiesen werden, die den Typ auf dem Stapel verwenden möchte. Genau genommen gibt es keinen Weg, dies zu tun. Am besten für uns geeignet ist jedoch stackalloc. Genau das richtige Keyword für unsere Zwecke. Leider war es das, was Unkontrollierbarkeit in den Code einführte. Zuvor gab es die Idee, Span für diese Zwecke zu verwenden und auf unsicheren Code zu verzichten. An unsicherem Code ist nichts auszusetzen, aber wie überall ist er kein Wundermittel und hat seine eigenen Anwendungsbereiche.

Nachdem wir einen Zeiger auf den Speicher des aktuellen Stapels erhalten haben, übergeben wir diesen Zeiger an eine Methode, aus der der Typ in Teilen besteht. Das ist alles, was zugehört hat - gut gemacht.

 unsafe class Program { public static void Main() { int* pointer = stackalloc int[2]; var a = StackInitializer.InitializeOnStack<StackReferenceType>(pointer); a.StubMethod(); Console.WriteLine(a.Field); Console.WriteLine(a); Console.Read(); } } 

Sie sollten dies nicht in realen Projekten verwenden. Die Methode, die Speicher auf dem Stapel zuweist, verwendet neues T (), das wiederum Reflektion verwendet, um den Typ auf dem Heap zu erstellen! Diese Methode ist also langsamer als die übliche Erstellung des Typs einmal, also 40-50.

Hier sehen Sie das gesamte Projekt.

Quelle: In einem theoretischen Exkurs wurden Beispiele aus dem Buch Sasha Goldstein - Pro .NET Performace verwendet

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


All Articles