【问题标题】:Struct memory hack to overlap object reference - Is it possible?构造内存黑客以重叠对象引用 - 有可能吗?
【发布时间】:2013-07-20 06:18:28
【问题描述】:

我猜这个问题的答案是“不可能,切换到 C++”。但我想我还是会把它扔出去。

我正在处理一个巨大的二叉树。我有一个结构数组来表示我在遍历树时用来帮助处理内存局部性的分支节点。

为了节省一点内存,从而提高缓存局部性,我正在考虑重叠叶节点的对象引用。该对象引用将指向所有叶子数据。基本上是这样的:

[StructLayout(LayoutKind.Explicit)]
struct BranchData
{
    [FieldOffset(0)] // 1 byte
    internal byte SplitIndex;
    [FieldOffset(1)] // 4 bytes
    internal float SplitValue;
    [FieldOffset(5)] // 4 bytes
    internal int LowIndex;
    [FieldOffset(9)] // 4 bytes
    internal int HighIndex;
    [FieldOffset(0)] // 8 bytes (We're working with x64 here)
    internal LeafData Node;
}

上面给出了以下运行时错误

无法从程序集中加载类型“BranchData” 'WindowsFormsApplication1,版本=1.0.0.0,文化=中性, PublicKeyToken=null' 因为它在偏移量 0 处包含一个对象字段 未正确对齐或被非对象字段重叠。

我可以使用一个单独的数组来存储叶子数据,并使用索引指向该数组,但是我有 2 次内存查找(对于那些肯定是遥远的内存区域)。一个用于叶子数组中的位置以获取引用,一个用于获取叶子数据。如果我能实现这种重叠,我就会摆脱其中一个查找。

我能够固定对象并使用不安全的代码来解决这个问题。速度是这里的关键因素。

【问题讨论】:

  • 我可能有一个答案,但我首先有几个问题: 1. 你能使用不安全的代码吗? 2. 如果不能使用不安全的代码,能否将对象固定在内存中?
  • @DanMcCann 我可以做到其中任何一个。我对代码的肮脏程度没有限制:-)
  • 我在这里延伸了这个问题,但你能把问题移到另一个地方吗?假设您使用Neo4j 作为您的树并获得所有已经编写好的查询。您可能出现的性能问题会在其他地方出现,但在那里可能更容易解决。 FWIW
  • 答案是,“不,你不能那样做。”运行时不允许您覆盖引用类型和值类型。我隐约记得曾在某一时刻找到规范的相关部分,但不记得在哪里。
  • @WillCalderwood 你有没有设法完成这项工作?

标签: c# .net performance memory-management


【解决方案1】:

此限制在托管代码中非常重要。问题是您的 Node 成员是对象引用。运行时的指针。它与其他字段重叠。

垃圾收集器需要能够找到该指针。两者都必须知道堆上存在对 LeafData 对象的实时引用。并在堆压缩时移动 LeafData 对象时更新该指针。

问题是:收集器无法判断您的联合是否存储了该指针。如果不是这样,则其他成员的值可能会看起来像对 GC 的有效对象引用。这非常非常糟糕。

存储不安全的 LeafData* 在技术上是可行的,但这需要固定 LeafData 对象。当树很大时,那是行不通的,当无法再移动任何东西时,GC 就会崩溃。将 LeafData 数据存储在非托管内存中更进一步,您将开始编写 C++ 代码。您唯一可以做的另一件事是将 LeafData 作为结构体存储在节点本身中,您不太可能对拟合感到满意。

请注意,您应该避免这些未对齐的字段,当字段跨越 L1 缓存行边界时,您会受到相当大的打击。将 SplitIndex 放在 HighIndex 之后,这样就不会发生这种情况。

【讨论】:

  • 谢谢,这是我所期待的答案。我希望我可以做一些肮脏的事情——可能有两种不同的结构类型,并使用指向 BranchData 实例的指针非法将我的第二种类型插入到我的 BranchData 数组中,确保它当然是正确的大小。一切就绪后,我可能会研究 C++ 重写以通过 hack 加快速度。
【解决方案2】:

我不知道这在实践中是否更快,但它在托管代码中的内存查找更少。

(在 CLR 本身中可能有更多我不知道的查找。)

也就是说,您可以使用GCHandle 将托管引用与非托管数据重叠:

[StructLayout(LayoutKind.Explicit)]
public struct Data
{
    [FieldOffset(0)]
    public IntPtr NativeData;
    [FieldOffset(0)]
    public GCHandle Handle;
}


Data data = ...;
((YourClass)data.Handle.Target).Blah();

【讨论】:

  • 问题不在于内存查找是托管还是非托管。这只是它们首先存在的事实。查看不在 CPU 缓存中的任何内容都非常慢,这就是为什么我需要尽可能多地塞进一个小空间。
  • 如果两个字段的偏移量相同,是不是会导致该内存位置的数据不一致?
  • @EhsanSajjad:重叠两个字段本身不是问题——您只需读取和写入重叠部分所代表的任何值。但是,在托管类型的情况下,如果它试图跟随一个实际上不是指针的指针,它可能会混淆垃圾收集器。已经 4 年了,所以我不记得写这个答案时脑子里在想什么;出于某种原因,我似乎认为垃圾收集器不会尝试遵循GCHandle,而是在其他地方使用查找表。所以我真的不能再保证了……如果我错了,希望有人能纠正我。
  • 我的意思是,如果我有一个带有 to int 的结构,并且两者的偏移量都为 0,这究竟意味着什么?
  • 我只是想了解FieldOffSet这个东西
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-09-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多