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

Dieser Artikel zeigt Ihnen die Grundlagen der internen Typen, als Beispiel, 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 fortfahren , empfehle ich Ihnen dringend , den ersten Beitrag über StructLayout zu lesen, da in diesem Artikel ein Beispiel verwendet wird (jedoch wie immer).

Vorgeschichte


Als ich anfing, Code für diesen Artikel zu schreiben, wollte ich etwas Interessantes mit Assemblersprache 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 den Werttypen dadurch unterscheidet, dass sich die ersten auf dem Heap und die zweiten auf dem Stapel befinden, entschied ich mich, einen Assembler zu verwenden, um zu zeigen, dass der Referenztyp auf dem leben kann Stapel. Ich bekam jedoch alle möglichen Probleme, zum Beispiel die Rückgabe der Adresse und ihrer Darstellung als verwalteter Link (ich arbeite noch daran). Also fing ich an zu schummeln und etwas zu tun, das in der Assemblersprache in C # nicht funktioniert. Und am Ende gab es überhaupt keinen Assembler.
Lesen Sie auch die Empfehlung - wenn Sie mit dem Layout von Referenztypen vertraut sind, empfehle ich, die Theorie über diese zu überspringen (nur die Grundlagen werden angegeben, nichts Interessantes).

Ein wenig über die Interna der Typen (für das alte Framework werden jetzt einige Offsets geändert, aber das Gesamtschema ist das gleiche)


Ich möchte daran erinnern, dass die Aufteilung des Speichers in einen Stapel und einen Heap auf .NET-Ebene erfolgt und diese Aufteilung rein logisch ist. Es gibt physikalisch keinen Unterschied zwischen den Speicherbereichen unter dem Heap und dem Stapel. Der Unterschied in der Produktivität wird nur durch unterschiedliche Algorithmen für die Arbeit mit diesen beiden Bereichen bereitgestellt.

Wie ordne ich dann Speicher auf dem Stapel zu? Lassen Sie uns zunächst verstehen, wie dieser mysteriöse Referenztyp angeordnet ist und was dieser Werttyp nicht hat.

Betrachten Sie also das einfachste Beispiel mit der Klasse Employee.

Code Mitarbeiter
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 wir uns an, wie es im Speicher dargestellt wird.
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 (Titel des Objektkopfworts im Bild) und die Adresse der Methodentabelle.

Das erste Feld (der Synchronisationsblockindex) interessiert uns nicht wirklich. Beim Platzieren des Typs habe ich beschlossen, ihn zu überspringen. 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. 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, ein paar Worte zu diesem Bereich zu sagen. Es wird für verschiedene Zwecke verwendet (Hash-Code, Synchronisation). Das Feld selbst ist vielmehr 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 (so etwas wie 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 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 der Polymorphismus möglich (das übrigens Strukturen, Stapelkönige, nicht besitzen).
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, alles wird gezeigt und alles ist klar. Zusammenfassend wird die virtuelle Methode nicht direkt nach Adresse, sondern nach dem Offset in der Methodentabelle aufgerufen. In der Hierarchie befinden sich dieselben virtuellen Methoden am selben Versatz in der Methodentabelle. Das heißt, in der Basisklasse rufen wir die Methode nach Offset auf, ohne zu wissen, welcher Methodentabellentyp verwendet wird, aber zu wissen, dass dieser Offset die relevanteste Methode für den Laufzeittyp ist.

Beachten Sie auch, dass die Objektreferenz nur auf den Methodentabellenzeiger verweist.

Lang erwartetes 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 einfache Mapper geschrieben - Zeiger auf verwaltete Typen und zurück. 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, machen Sie ihn auf eine Weise in zwei Richtungen.

Code der Mapper
 // Provides the signatures we need public class PointerCasterFacade { public virtual unsafe T GetManagedReferenceByPointer<T>(int* pointer) => default(T); public virtual unsafe int* GetPointerByManagedReference<T>(T managedReference) => (int*)0; } // Provides the logic we need 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; } 



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

Um das Auffinden der Adresse der Methodentabelle zu vereinfachen, 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. Ferner erhalten wir unter Verwendung der zuvor beschriebenen Konverter einen Zeiger auf den erstellten Typ.

Dieser Zeiger zeigt genau auf die Methodentabelle. Daher ist es ausreichend, den Inhalt einfach aus dem Speicher abzurufen, auf den er zeigt. 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 dort schreiben, wo sie zeigt.

Eigentlich ist das alles. Plötzlich richtig? Jetzt ist unser Typ fertig. Pinocchio, der uns Speicher zugewiesen hat, kümmert sich selbst um die Initialisierung der Felder.

Es bleibt nur, unseren Ultra-Mega-Caster zu verwenden, 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 schwarzer Erde 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. Über Aufrufe von virtuellen Methoden und Sprache kann nicht sein, die Ausnahme wird auftreten. Normale Methoden werden direkt aufgerufen. Der Code enthält nur Adressen für echte Methoden, sodass sie funktionieren. Und anstelle der Felder wird ... und niemand weiß, was dort 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 überschrieben wird), muss die Methode, die den Typ auf den Stapel anwenden möchte, Speicher zuweisen. Genau genommen gibt es einige Möglichkeiten, dies zu tun. Am besten für uns geeignet ist jedoch stackalloc . Genau das richtige Keyword für unsere Zwecke. Leider bringt es das Unsichere in den Code. Zuvor gab es die Idee, Span für diese Zwecke zu verwenden und auf unsicheren Code zu verzichten. Im unsicheren Code gibt es nichts Schlechtes, aber wie überall ist es kein Wundermittel und hat seine eigenen Anwendungsbereiche.

Nachdem wir den Zeiger auf den Speicher des aktuellen Stapels erhalten haben, übergeben wir diesen Zeiger an die 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 es nicht in realen Projekten verwenden. Die Methode zum Zuweisen von Speicher auf dem Stapel verwendet neues T (), das wiederum Reflektion verwendet, um einen Typ auf dem Heap zu erstellen! Diese Methode ist also langsamer als die übliche Erstellung der Zeiten in 40-50. Darüber hinaus ist es nicht plattformübergreifend.

Hier finden Sie das gesamte Projekt.

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

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


All Articles