打破C#的基础:在堆栈上为引用类型分配内存

在本文中,将提供内部类型设备的基础知识,以及一个示例,其中将在堆栈上完全分配引用类型的内存(这是因为我是全栈程序员)。



免责声明


本文不包含实际项目中应使用的材料。 它只是感觉到编程语言的界限的扩展。

在开始讲故事之前,我强烈建议您阅读有关StructLayout的第一篇文章,因为 这里分析了一个将在本文中使用的示例(但是,一如既往)。

背景知识


开始编写本文的代码后,我想使用汇编语言做一些有趣的事情。 我想以某种方式打破标准执行模型,并得到一个非常不寻常的结果。 记住人们说引用类型与重要类型的频率不同之处在于,第一个位于堆上,第二个位于堆栈上,我决定使用汇编程序来表明引用类型可以存在于堆栈中。 但是,我开始遇到各种各样的问题,例如,返回所需的地址并将其表示为托管链接(我仍在努力)。 因此,我开始欺骗并执行在C#中的汇编程序中不起作用的事情。 最后,汇编器根本没有保留。
另外,推荐阅读-如果您熟悉引用类型的设备,我建议您跳过有关它们的理论(仅提供基础知识,没有什么有趣的意思)。

关于类型的内部结构


我想提醒您,堆栈和堆上的内存分离发生在.NET级别,并且这种划分纯粹是逻辑上的,在物理上,堆下和堆栈下的内存区域之间没有区别。 通过与这些领域合作,已经特别提供了生产率的差异。

然后如何在堆栈上分配内存? 首先,让我们看一下这个神秘的引用类型的结构以及它的含义,这并不重要。

因此,请考虑Employee类的最简单示例。

员工守则
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..."); } } 


并看看它在内存中的呈现方式。
UPD:在32位系统的示例中考虑此类。



因此,除了用于这些字段的内存外,我们还有两个其他隐藏字段-同步块的索引(图片中对象的标题词)和方法表的地址。

第一个字段是同步块的索引,我们并不特别感兴趣。 放置字体时,我决定忽略它。 我这样做有两个原因:

  1. 我很懒惰(我没有说理由会合理)
  2. 对于对象的基本功能,此字段是可选的

但是,既然我们已经讲过话,我认为对这一领域说几句话是正确的。 它用于不同的目的(哈希码,同步)。 而是,字段本身只是与此对象关联的同步块之一的索引。 块本身位于同步块表(全局数组)中。 创建这样的块是一个相当大的操作,因此如果不需要它就不会创建。 此外,当使用细锁时,接收到该锁的线程的标识符(而不是索引)将被写入那里。

第二个领域对我们来说更为重要。 多亏了类型方法表,多态性之类的强大工具才有可能(顺便说一下,堆栈的国王并不拥有这种结构)。 假设Employee类另外实现了三个接口:IComparable,IDisposable和ICloneable。

然后方法表将如下所示



画面非常酷,原则上,所有内容均已绘制且易于理解。 如果手指不方便,则不会直接在地址处调用虚拟方法,而是通过方法表中的偏移量进行调用。 在层次结构中,相同的虚拟方法将位于方法表中的相同偏移处。 也就是说,我们在偏移量处调用基类上的方法,而不知道将使用哪种类型的方法表,但是知道在此偏移量处将存在与运行时类型最相关的方法。

还应记住,对对象的引用指向方法表。

期待已久的例子


让我们从有助于我们实现目标的课程开始。 使用StructLayout(我确实尝试过使用它,但是没有成功),我写了最简单的指向托管类型的指针映射器,反之亦然。 从托管链接获取指针很容易,但是反向转换给我带来了困难,并且我三思而后行地应用了我喜欢的属性。 为了将代码保持在一个键中,我以一种方式在两个方向上进行了编码。

在这里编码
 //     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; } 


首先,编写一个方法,该方法采用指向某个内存的指针(顺便说一句,不一定要在堆栈上)并配置类型。

为了便于查找方法表的地址,我在堆上创建一个类型。 我确信可以以其他方式找到方法表,但是我没有设定优化该代码的目标,对我来说,使其变得可理解更有趣。 接下来,使用前面描述的转换器,我们得到一个指向所创建类型的指针。

该指针恰好指向方法表。 因此,仅从它指向的内存中获取内容就足够了。 这将是方法表的地址。
而且由于传递给我们的指针是对对象的一种引用,因此我们必须准确记录方法表指向的地址。

实际上,仅此而已。 没想到吧? 现在我们的类型准备好了。 分配给我们内存的Pinocchio将负责字段的初始化。

仍然仅使用Grandcaster将指针转换为托管链接。

 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; } } 

现在,我们在堆栈上有一个指向同一堆栈的链接,根据所有引用类型的定律(几乎),该对象位于由黑土和木棍构成的对象中。 多态是可用的。

应该理解的是,如果您将此链接传递到方法之外,那么从它返回之后,我们将得到一些不清楚的地方。 不能谈论对虚拟方法的调用;让我们例外。 常规方法被直接调用,在代码中将只包含实际方法的地址,因此它们将起作用。 在田地的地方将会是……但是没人知道那里会是什么。

由于无法在堆栈上使用单独的方法进行初始化(因为从该方法返回后堆栈帧将被擦除),因此应使用想要在堆栈上使用类型的方法来分配内存。 严格来说,没有一种方法可以做到这一点。 但是最适合我们的是stackalloc。 仅仅是我们目的的完美关键字。 不幸的是,正是在代码中引入了不可控制性。 在此之前,有一个想法将Span用于这些目的,并且没有不安全的代码。 不安全的代码没有什么错,但是像其他地方一样,它不是万灵丹,它有自己的应用领域。

接下来,在接收到指向当前堆栈上的内存的指针之后,我们将此指针传递给组成部分类型的方法。 那就是所有听的人-做得好。

 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(); } } 

您不应该在实际项目中使用此方法,在堆栈上分配内存的方法使用新的T(),后者又使用反射在堆上创建类型! 因此,此方法将比通常的类型创建速度慢40-50。

在这里您可以看到整个项目。

资料来源:从理论上讲,例子来自书Sasha Goldstein-Pro .NET Performace

Source: https://habr.com/ru/post/zh-CN428676/


All Articles