Je ne respecte pas l'encapsulation ou l'utilisation d'un autre type de table de méthodes pour appeler rapidement des méthodes privées

Bonjour à tous. Je voudrais partager un exemple d'utilisation de StructLayout pour quelque chose de plus intéressant que des exemples d'octets, d'entiers et d'autres nombres, dans lesquels tout se passe un peu plus que prévu.

Avant de se lancer dans une violation d'encapsulation ultra-rapide, il convient de rappeler ce qu'est StructLayout. À strictement parler, il s'agit même d'un StructLayoutAttribute, c'est-à-dire un attribut qui vous permet de créer des structures et des classes similaires à l'union en C ++. Plus en détail, cet attribut vous permet de prendre le contrôle du placement des membres de la classe en mémoire. En conséquence, il est placé au-dessus de la classe. Habituellement, si une classe a 2 champs, nous nous attendons à ce qu'ils soient disposés séquentiellement, c'est-à-dire qu'ils seront indépendants les uns des autres (ne se chevauchent pas). Cependant, StructLayout permet de spécifier que l'emplacement des champs sera déterminé non pas par l'environnement, mais par l'utilisateur. Pour indiquer explicitement les décalages de champ, utilisez le paramètre LayoutKind.Explicit. Pour indiquer à quel décalage par rapport au début de la classe / structure (ci-après dénommée la classe) nous voulons placer le champ, nous devons placer l'attribut FieldOffset au-dessus de lui, qui prend en paramètre le nombre d'octets - le retrait du début de la classe. Une valeur négative ne peut pas être transmise, alors ne pensez même pas à ruiner les pointeurs vers la table de méthode ou l'index du bloc de synchronisation;

Commençons à écrire le code. Pour commencer, je suggère de commencer par un exemple simple. Créez une classe du formulaire suivant:
public class CustomClass { public override string ToString() { return "CUSTOM"; } public virtual object Field { get; } = new object(); } 

Ensuite, nous utilisons le mécanisme ci-dessus pour définir explicitement les décalages de champ.
 [StructLayout(LayoutKind.Explicit)] public class CustomStructWithLayout { [FieldOffset(0)] public string Str; [FieldOffset(0)] public CustomClass SomeInstance; } 

Pour l'instant, je vais reporter les explications et utiliser la classe écrite comme suit:
  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(); } } 

Total L'appel à la méthode GetType () renvoie une chaîne, la méthode ToString () est coquine et nous donne la chaîne «4564».
Décharge pour le cerveau: qu'est-ce qui sera affiché lorsque la propriété virtuelle CustomClass sera appelée?

Comme vous l'avez peut-être deviné, nous avons initialisé CustomStructWithLayout, les deux liens sont nuls, puis nous initialisons un champ de notre type, puis nous attribuons une chaîne au champ Str. En conséquence, il reste un peu plus de CustomClass que rien. En plus, il y avait une ligne avec toute sa structure interne, y compris un tableau des méthodes et un index d'un bloc de synchronisation. Mais le compilateur voit que le champ est toujours du type de notre classe.
Pour preuve, je vais donner un petit extrait de WinDbg:

Ici, vous pouvez voir des choses inhabituelles. Le premier est que dans l'objet, les adresses sur les tables de méthodes ont des champs différents pour les champs de classe, comme prévu, mais l'adresse de la valeur de champ est un. La seconde - vous pouvez voir que les deux champs sont situés à l'offset 4. Je pense que la plupart comprendront, mais juste au cas où, je vais expliquer, directement à l'adresse de l'objet, il y a un lien vers le tableau des méthodes. Les champs commencent par un décalage de 4 octets (pour z 32 bits) et l'index du bloc de synchronisation est localisé avec un décalage de -4.

Maintenant que vous avez compris ce qui se passe, vous pouvez essayer d'utiliser des décalages pour appeler quelque chose que vous ne devriez pas appeler.
Pour ce faire, j'ai répété la structure de la classe string dans l'une de mes classes. Certes, je n'ai répété que le début, car la classe de chaîne est très volumineuse.
  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; } } 

Eh bien, la structure avec Layout change un peu
  [StructLayout(LayoutKind.Explicit)] public class CustomStructWithLayout { [FieldOffset(0)] public string Str; [FieldOffset(0)] public CustomClassLikeString SomeInstance; } 

De plus, lors de l'appel de FakeLength ou de la méthode CompareTo (), en raison du décalage identique de ces membres de classe par rapport à l'adresse de l'objet lui-même, la méthode de chaîne correspondante (dans ce cas) sera appelée. Il a fallu un certain temps pour arriver à la première méthode privée de la ligne que je peux utiliser, alors j'ai opté pour la méthode publique. Mais le terrain est privé, tout est juste. Soit dit en passant, les méthodes sont rendues virtuelles pour se protéger contre toutes sortes d'optimisations, ce qui interfère avec le travail (par exemple, l'incorporation), et aussi pour que la méthode soit appelée par un décalage dans la table des méthodes.

Donc, la performance. C'est un fait qu'un concurrent direct dans la contestation de ce qu'il n'est pas nécessaire de provoquer et dans la violation de l'encapsulation est la réflexion. Je pense qu'il est déjà clair que nous sommes plus rapides que cette chose, nous n'analysons pas tous les métadonnées. Valeurs exactes:
La méthodeJobMoyenneErreurStddevMédiane
StructLayoutFieldClr0,0597 ns0,0344 ns0,0396 ns0,0498 ns
ReflectionFieldClr197,1257 ns1,9148 ns1,7911 ns197,4787 ns
StructLayoutMethodClr3,5195 ns0,0382 ns0,0319 ns3,5285 ns
ReflectionMethodClr743,9793 ns13,7378 ns12.8504 ns743.8471 ns
Voici un long morceau de code avec la façon dont j'ai mesuré les performances (si quelqu'un en a besoin):
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>(); } } 

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


All Articles