大家好 我想分享一个使用StructLayout的示例,而不是带有字节,整数和其他数字的示例,其中发生的一切都比预期的多。
简而言之,在进行闪电般的封装违反之前,值得回顾一下StructLayout是什么。 严格来说,这甚至是一个StructLayoutAttribute,即允许您创建类似于C ++中的union的结构和类的属性。 更详细地讲,此属性使您可以控制类成员在内存中的放置。 因此,它被放置在类之上。 通常,如果一个类有2个字段,我们希望它们按顺序排列,也就是说,它们将彼此独立(不重叠)。 但是,使用StructLayout可以指定字段的位置不是由环境而是由用户确定。 若要明确指示字段偏移量,请使用LayoutKind.Explicit参数。 为了表明要相对于类/结构的开头(以下称为类)的起始偏移量,我们应将FieldOffset属性放在其上方,该属性将字节数(从类开头开始的缩进)作为参数。 您将无法传递负值,因此甚至不用考虑破坏指向方法表的指针或同步块的索引,这会变得有些复杂。
让我们开始编写代码。 首先,我建议从一个简单的例子开始。 创建以下形式的类:
public class CustomClass { public override string ToString() { return "CUSTOM"; } public virtual object Field { get; } = new object(); }
接下来,我们使用上述机制来显式设置字段偏移量。
[StructLayout(LayoutKind.Explicit)] public class CustomStructWithLayout { [FieldOffset(0)] public string Str; [FieldOffset(0)] public CustomClass SomeInstance; }
现在,我将推迟解释,并按如下所示使用书面课程:
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(); } }
合计 调用GetType()方法将返回一个字符串,ToString()方法很顽皮,并为我们提供了字符串“ 4564”。
释放大脑:调用虚拟属性CustomClass时会显示什么?
您可能已经猜到了,我们初始化了CustomStructWithLayout,两个链接都为空,然后我们初始化了一个类型的字段,然后为Str字段分配了一个字符串。 结果,CustomClass剩下的比什么都没有。 最上面的一行是其所有内部结构,包括一个方法表和一个同步块索引。 但是编译器看到该字段仍然是我们类的类型。
为了证明这一点,我将对WinDbg进行一些剪裁:

在这里您可以看到一些不寻常的东西。 首先是,在对象中,方法表上的地址具有与类字段相同的字段,这与预期的一样,但字段值的地址为1。 第二个-您可以看到两个字段都位于偏移量4处。我想大多数人都可以理解,但是以防万一,我将在对象的地址处直接说明方法表的链接。 字段以4字节的偏移量开始(对于z 32位),并且同步块的索引以-4的偏移量定位。
现在,您已经知道发生了什么,可以尝试使用偏移量调用不应该调用的东西。
为此,我在一个类中重复了字符串类的结构。 是的,我只重复了开头,因为字符串类非常庞大。
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; } }
好吧,“布局”的结构有些变化
[StructLayout(LayoutKind.Explicit)] public class CustomStructWithLayout { [FieldOffset(0)] public string Str; [FieldOffset(0)] public CustomClassLikeString SomeInstance; }
此外,当调用FakeLength或CompareTo()方法时,由于这些类成员相对于对象本身的地址的偏移相同,将调用相应的字符串方法(在这种情况下)。 我花了相当长的时间才能找到该行可以使用的第一个私有方法,所以我选择了公开的方法。 但是领域是私有的,一切都是公平的。 顺便说一句,这些方法被设置为虚拟的,以防止受到各种优化的影响,这些优化会干扰工作(例如嵌入),并因此使方法表中的偏移量调用该方法。
因此,性能。 事实上,在没有必要引起挑战和违反封装的挑战中,直接竞争者就是反思。 我认为我们已经比这件事更快了,我们所有人都不分析元数据。 确切值:
方法 | 工作机会 | 均值 | 失误 | 标准差 | 中位数 |
---|
StructLayoutField | 清除率 | 0.0597 ns | 0.0344 ns | 0.0396 ns | 0.0498 ns |
反射场 | 清除率 | 197.1257 ns | 1.9148 ns | 1.7911 ns | 197.4787 ns |
StructLayoutMethod | 清除率 | 3.5195 ns | 0.0382 ns | 0.0319 ns | 3.5285 ns |
反射法 | 清除率 | 743.9793 ns | 13.7378 ns | 12.8504 ns | 743.8471 ns |
这是一长段代码,介绍了我如何评估性能(如果有人需要):
代号 [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>(); } }