如何在.NET Core中分配连续和固定内存

3

我正在从事一项物理模拟项目,性能非常重要,而我认为我的内存管理是瓶颈之一。目前,我有包含固定数量刚体、粒子和力的缓冲区对象。在模拟开始之前,所有的物理实体都在其缓冲区内进行初始化。当需要一个实体时,选择第一个非活动的实体,否则选择最旧的实体;当它不再需要时,将其移动到末尾,这样活动的实体就都排在前面。

这是我正在使用的数据结构。

public sealed class Buffer<TValue> : IEnumerable<BufferElement<TValue>> where TValue : new()
{
    public Buffer(int capacity)
    {
        Count = 0;
        Capacity = capacity;
        Elements = new BufferElement<TValue>[capacity];

        for (var index = 0; index < Elements.Length; index++)
        {
            Elements[index] = new BufferElement<TValue>();
        }
    }

    public int Count { get; private set; }

    public int Capacity { get; private set; }

    private int ActiveCount { get; set; }

    private BufferElement<TValue>[] Elements { get; }

    public BufferElement<TValue> Activate()
    {
        if (Count == ActiveCount) Count = 0;

        var bufferElement = Elements[Count++];

        if (!bufferElement.Active)
        {
            bufferElement.Active = true;
            ActiveCount++;
        }

        return bufferElement;
    }

    public void Deactivate(BufferElement element)
    {
        if (!element.Active) return;

        element.Active = false;

        var lhs = element.Index;
        var rhs = --ActiveCount;

        Elements[lhs] = Elements[rhs];
        Elements[rhs] = element;

        Elements[lhs].Index = lhs;
        Elements[rhs].Index = rhs;
    }
}

阅读了.NET Core处理数组的相关内容后,有两个问题可能会引起注意。第一个是每次访问数组中的元素时,都会执行安全检查,而第二个则是GC可能会将数组复制到新的内存地址。
如果可能的话,我希望所有包含物理实体的缓冲区都不进行任何安全检查,并且在连续的内存中固定。我相信这应该是可能的,因为每个缓冲区的大小都是固定的,元素的大小(刚体、粒子、力)也是固定的。
在C#中似乎有很多管理内存的方法,我很难确定在这种情况下哪种方法对我来说是正确的。
现在,问题分为三个部分:
1.是否可以做到? 2.如果可以,最好的内存管理方法是什么? 3.适当的实现应该是什么样子?

我还没有考虑使用GPU。你是指将实体存储在GPU上,还是利用GPU进行模拟? - Jedi_Maseter_Sam
1
数组在连续的内存中布局。但是它们的内容可能不是:因为您正在修改BufferElement,我假设它是一个类,在这种情况下,数组将包含指针,而不是元素本身。我不清楚结构体是否会直接放入数组中,但如果是这样,它很可能会受到直接复制优化的类似限制。顺序布局仅在您对顺序元素执行足够数量(成比例)的工作时才有用-如果元素直接放入数组中,则这更容易实现。 - Clockwork-Muse
这是一个很好的观点。BufferElement确实是一个类,所以你是正确的,数组将是一个包含指针的连续内存块。由于物理实体不断被迭代,使它们在内存中顺序排列肯定会减少缓存未命中。你知道如何确保数组的内容也是连续的吗? - Jedi_Maseter_Sam
@Jedi_Maseter_Sam - 它们在堆空间中,因此您需要一个自定义分配器,这在C/C++领域被认为是(有点)高级话题,并且在C#中很可能不可能实现。 - Clockwork-Muse
@Clockwork-Muse 不要理会这些错误,我不小心从旧分支复制了代码。等我回到电脑前我会修复它们的。关于内存分配,Marshal 不会分配连续的内存吗? - Jedi_Maseter_Sam
显示剩余9条评论
1个回答

1

首先,是的,这是可行的。其次,最好的方法真的取决于各种权衡,因此很快就会变成一篇主观的文章。第三,最轻量级的选择是在unsafe上下文中使用fixed关键字来分配您的数组(不会对数组进行任何安全检查,并允许您使用C风格指针)并固定其地址,以便它不改变,尽管使用SpanMemory可能更容易,但fixedunsafe的低级方法在正确使用时可以提供更好的性能。查看this,它是官方文档,并且充满了有用的示例。最后一个提示是,如果可能的话,请尝试切换到struct,它们没有内存开销和更高的内存密度,因为更多内容将适合缓存而产生更好的缓存访问时间。


这就是为什么我说“如果可能的话”。然而,一个 struct 数组被作为引用传递。即使它们包含值类型,数组也是引用类型。此外,请记住您可以使用 ref 关键字传递值类型的引用并避免复制。 - Javier Silva Ortíz
是的,使用SpanMemoryfixedunsafe更高级。我以为你想要尽可能多的原始性能和控制。 - Javier Silva Ortíz
我的意思是,像array[index]这样访问数组会产生索引处值的副本。此外,我不知道您可以从函数中返回一个ref,我认为这仅适用于传入参数。 - Jedi_Maseter_Sam
我想我不理解fixed的地方是你应该把它放在哪里。如果你将其放置在构造函数中并在fixed语句中设置一个字段,GC是否仍然控制内存? - Jedi_Maseter_Sam
1
GC 将控制内存,“fixed” 表示该变量不能移动到另一个内存位置。自 C# 7 开始,有一个名为“ref return”的功能,您可以将 ref array[index] 传递给具有入站 ref 的方法,并且该特定值是通过引用传递的。 - Javier Silva Ortíz
显示剩余4条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接