Ich respektiere weder die Kapselung noch die Verwendung der Methodentabelle eines anderen Typs für den schnellen Aufruf der privaten Methoden

Hallo. Ich möchte Ihnen ein Beispiel für die Verwendung von StructLayout für etwas Interessanteres als Beispiele mit Bytes, Ints und anderen primitiven Typen zeigen, wenn alles ganz offensichtlich geschieht.



Bevor Sie mit der blitzschnellen Verletzung der Kapselung fortfahren, sollten Sie kurz daran erinnern, was StructLayout ist. Genau genommen ist es sogar StructLayoutAttribute , ein Attribut, mit dem Sie Strukturen und Klassen erstellen können, die der Vereinigung in C ++ ähneln. Mit diesem Attribut können Sie die Platzierung von Klassenmitgliedern im Speicher steuern (mithilfe von Offsets). Dementsprechend wird es über der Klasse platziert.

Wenn eine Klasse zwei Felder hat, erwarten wir normalerweise, dass diese nacheinander angeordnet werden, dh sie sind unabhängig voneinander (nicht überlappen). Mit StructLayout können Sie jedoch festlegen, dass der Speicherort der Felder nicht von der Umgebung, sondern vom Benutzer festgelegt wird. Um den Versatz der Felder explizit anzugeben, sollten wir den Parameter LayoutKind.Explicit verwenden .

Um anzugeben, welchen Versatz vom Anfang der Klasse / Struktur (im Folgenden nur "Klasse") wir das Feld platzieren möchten, müssen wir das FieldOffset- Attribut darauf setzen. Dieses Attribut verwendet als Parameter die Anzahl der Bytes - den Versatz vom Beginn der Klasse. Es ist unmöglich, einen negativen Wert zu übergeben, um die Zeiger auf die Methodentabelle oder den Synchronisationsblockindex nicht zu verderben. Es wird also etwas komplizierter.

Beginnen wir mit dem Schreiben des Codes. Zunächst schlage ich vor, mit einem einfachen Beispiel zu beginnen. Erstellen Sie folgende Klasse:

public class CustomClass { public override string ToString() { return "CUSTOM"; } public virtual object Field { get; } = new object(); } 

Als nächstes verwenden wir den oben beschriebenen Mechanismus, um Feldversätze explizit anzugeben.

  [StructLayout(LayoutKind.Explicit)] public class CustomStructWithLayout { [FieldOffset(0)] public string Str; [FieldOffset(0)] public CustomClass SomeInstance; } 

Im Moment werde ich die Erklärungen verschieben und die schriftliche Klasse wie folgt verwenden:

  class Program { static void Main(string[] args) { CustomStructWithLayout instance = new CustomStructWithLayout(); instance.SomeInstance = new CustomClass(); instance.Str = "4564"; Console.WriteLine(instance.SomeInstance.GetType()); //System.String Console.WriteLine(instance.SomeInstance.ToString()); //4564 Console.Read(); } } 

Wenn Sie die GetType () -Methode aufrufen, wird eine Zeichenfolge zurückgegeben. Die ToString () -Methode ist ungezogen und gibt uns die Zeichenfolge "4564".

Gehirnentladung: Was wird nach dem Aufruf der virtuellen CustomClass- Eigenschaft angezeigt?

Wie Sie bereits vermutet haben, haben wir CustomStructWithLayout initialisiert. Beide Links sind null. Dann initialisieren wir das Feld unseres Typs und weisen die Zeichenfolge dem Feld Str zu. Infolgedessen verweist der CustomClass- Link nicht auf das CustomClass- Objekt, sondern auf das System.string-Objekt (einschließlich der Methodentabelle und des Index der Synchronisationseinheit). Der Compiler sieht jedoch, dass das Feld immer noch vom Typ unserer Klasse ist.

Zum Beweis hier ein kleiner Ausschnitt aus WinDbg:



Hier können Sie einige ungewöhnliche Dinge sehen.

  • In CustomStructWithLayout haben Objektfelder unterschiedliche Adressen von Methodentabellen (sehr zu erwarten), aber die Adressen von Objekten sind gleich.
  • Das zweite ist, dass Sie sehen können, dass sich beide Felder am Versatz 4 befinden. Ich denke, die meisten werden verstehen, aber nur für den Fall, ich werde erklären, direkt an die Adresse des Objekts einen Link zur Methodentabelle platziert. Die Felder beginnen mit einem Versatz von 4 Bytes (für 32 Bit), und der Index des Synchronisationsblocks befindet sich mit einem Versatz von -4. Somit haben beide Objekte den gleichen Versatz.

Nachdem Sie herausgefunden haben, was passiert, können Sie versuchen, mithilfe von Offsets aufzurufen, was nicht hätte aufgerufen werden sollen.

Dazu habe ich die Struktur der String-Klasse in einer meiner Klassen wiederholt. Aber ich habe nur den Anfang wiederholt, da die Klassenfolge ziemlich umfangreich ist und ich sehr faul bin.

Hinweis: Konstanten und Statiken sind nicht erforderlich, nur zum Spaß.

  public class CustomClassLikeString { public const int FakeAlignConst = 3; public const int FakeCharPtrAlignConst = 3; public static readonly object FakeStringEmpty; public char FakeFirstChar; public int FakeLength = 3; public const int FakeTrimBoth = 3; public const int FakeTrimHead = 3; public const int FakeTrimTail = 3; public CustomClassLikeString(){} public CustomClassLikeString(int a){} public CustomClassLikeString(byte a){} public CustomClassLikeString(short a){} public CustomClassLikeString(string a){} public CustomClassLikeString(uint a){} public CustomClassLikeString(ushort a){} public CustomClassLikeString(long a){ } public void Stub1() { } public virtual int CompareTo(object value) { return 800; } public virtual int CompareTo(string value) { return 801; } } 

Nun, die Struktur mit dem Layout wird ein wenig geändert.

  [StructLayout(LayoutKind.Explicit)] public class CustomStructWithLayout { [FieldOffset(0)] public string Str; [FieldOffset(0)] public CustomClassLikeString SomeInstance; } 

Wenn Sie FakeLength oder die CompareTo () -Methode aufrufen, wird aufgrund des identischen Versatzes dieser Klassenmitglieder relativ zur Adresse des Objekts selbst die entsprechende Zeichenfolgenmethode aufgerufen (in diesem Fall).

Das Aufrufen der ersten privaten Methode der Zeichenfolge, die ich verwenden kann, war zu lang, sodass ich bei einer öffentlichen angehalten habe. Aber das Feld ist privat, alles ist ehrlich. Übrigens werden die Methoden virtuell gestaltet, um sie vor Optimierungen zu schützen, die die Arbeit beeinträchtigen (z. B. Einbetten), und um die Methode durch den Offset in der Methodentabelle aufzurufen.

Also Leistung. Es ist klar, dass ein direkter Konkurrent beim Aufrufen von Dingen, die nicht aufgerufen werden sollten, und bei Verstößen gegen die Kapselung Reflexion ist. Ich denke, es ist klar, dass wir schneller sind als dieses Ding, weil wir keine Metadaten analysieren. Genaue Werte:
MethodeJobMittelwertFehlerStddevMedian
StructLayoutFieldClr0,0597 ns0,0344 ns0,0396 ns0,0498 ns
ReflectionFieldClr197,1257 ns1,9148 ns1,7911 ns197,4777 ns
StructLayoutMethodClr3,5195 ns0,0382 ns0,0319 ns3,5285 ns
ReflectionMethodClr743,9793 ns13.7378 ns12.8504 ns743,8471 ns


Hier ist ein langer Code, wie ich die Leistung gemessen habe (wenn jemand sie benötigt):
Code
  [ClrJob] [RPlotExporter, RankColumn] [InProcessAttribute] public class Benchmarking { private CustomStructWithLayout instance; private string str; [GlobalSetup] public void Setup() { instance = new CustomStructWithLayout(); instance.SomeInstance = new CustomClassLikeString(); instance.Str = "4564"; str = "4564"; } [Benchmark] public int StructLayoutField() { return instance.SomeInstance.FakeLength; } [Benchmark] public int ReflectionField() { return (int)typeof(string).GetField("m_stringLength", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(str); } [Benchmark] public int StructLayoutMethod() { return instance.SomeInstance.CompareTo("4564"); } [Benchmark] public int ReflectionMethod() { return (int)typeof(string).GetMethod("CompareTo", new[] { typeof(string) }).Invoke(str, new[] { "4564" }); } } class Program { static void Main(string[] args) { var summary = BenchmarkRunner.Run<Benchmarking>(); } } 


Russische Version

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


All Articles