【问题标题】:How can I reduce the garbage generation in this situation在这种情况下如何减少垃圾的产生
【发布时间】:2012-07-08 13:45:18
【问题描述】:

我的游戏已经到了产生过多垃圾并导致 GC 时间过长的地步。我一直在四处走动,减少了很多产生的垃圾,但是有一个地方过于频繁地分配了大量内存,我一直不知道如何解决这个问题。

我的游戏是一个 Minecraft 类型的世界,在你行走时会生成新的区域。我有一个大的、可变大小的数组,它是在创建用于存储地形顶点数据的新区域时分配的。数组填充数据后,将其传递给 slimdx DataStream,以便用于渲染。

问题在于这是一个可变大小的数组,需要将它传递给 slimdx,它会在其上调用 GCHandle.Alloc。由于它的大小可变,因此可能必须调整大小才能重用它。我也不能只为每个区域分配一个最大大小的数组,因为它需要大量的内存。由于与 slimdx 的 GCHandle 业务,我无法使用列表。

到目前为止,仅在需要增大数组时才调整数组的大小对我来说似乎是唯一可行的选择,但它可能效果不佳,实施起来可能会很痛苦。我需要单独跟踪数组的实际大小,并使用不安全的代码来获取指向数组的指针并将其传递给 slimdx。它最终也可能最终使用大量内存,以至于我不得不偶尔将所有数组的大小减小到所需的最小值。

我对这个解决方案犹豫不决,想知道是否有人对此有更好的解决方案。

【问题讨论】:

  • 你确定只是创建数组吗?数组是否包含类对象或结构?如果它确实包含类,则可能是您确实创建了一个大而复杂的对象图,这将花费 GC 更长的时间来检查垃圾。
  • 它是一个结构数组。该结构包含 3 个结构 (Vector3) 和 2 个浮点数。 CLR 分析器分配图将此结构列为最大的违规者,将近 300MB (25%)。

标签: c# garbage-collection slimdx


【解决方案1】:

我建议与 slimdx 库进行更紧密的集成。它是开源的,因此您可以深入挖掘并找到渲染所需的关键路径。然后,您可以使用 DMA 风格的内存共享方法进行更紧密的集成。

【讨论】:

  • 那么 slimdx 允许您通过 3 种方式创建 DataStream:传递一个大小并分配一个后备存储,传递一个数组并使用它,或者传递一个指针。既然我已经在用数组的方式了,那不是已经是内存共享了吗?
  • 稍微检查一下,确保库没有做一些奇怪的事情,比如一旦你传递它就将一半数组复制到另一个数组。然后取消分配那个半大小的临时数组并造成大量垃圾。
  • 另外,地形顶点图占用 300MB 似乎有些过分。如果您隔离仅用于渲染的关键路径并删除slimdx对结构其他部分的任何检查,然后将其从结构中删除,那么您必须利用稀疏性。
  • 在一些比较多山的地区,口吃最严重的地方,可以为地形加载多达 280 万个顶点。每个顶点占用 44 个字节。快速移动时,游戏每秒加载约 15MB。 300MB 大约持续了 20 秒左右。
  • 另外,关于 slimdx 做任何奇怪的分配工作:这里只涉及 2 个类,DataStream 和 Buffer。我注释掉了缓冲区的创建并且口吃仍然存在。我查看了 DataStream 的代码,并确认我正在使用的重载,它没有分配任何东西。
【解决方案2】:

由于 SlimDX 是开源的,而且速度太慢,现在是时候更改开源以满足您的性能需求了。我在这里看到的是,您希望保留一个更大的数组,但只将实际使用的区域交给 SlimDX,以防止为这个潜在的巨大数组分配额外的内存。

.NET Framework 中有一个名为ArraySegment 的类型正是为此目的而创建的。

// Taken from MSDN
// Create and initialize a new string array.
String[] myArr = { "The", "quick", "brown", "fox", "jumps", "over", "the", 
                   "lazy", "dog" };

// Define an array segment that contains the middle five values of the array.
ArraySegment<String> myArrSegMid = new ArraySegment<String>( myArr, 2, 5 );


public static void PrintIndexAndValues( ArraySegment<String> arrSeg )  
{
   for ( int i = arrSeg.Offset; i < (arrSeg.Offset + arrSeg.Count); i++ )  
   {
        Console.WriteLine( "   [{0}] : {1}", i, arrSeg.Array[i] );
   }
   Console.WriteLine();
}

也就是说,我发现 ArraySegment 的用法有些奇怪,因为我总是必须使用偏移量和索引,它们的行为不是常规数组。相反,您可以提取自己的结构,该结构允许基于零的索引,这更容易使用,但代价是每个基于索引的访问都会花费您并添加基本偏移量。但如果使用模式主要是 foreaches 那就无所谓了。

我遇到过 ArraySegment 也太昂贵的情况,因为您确实每次都分配一个结构并按堆栈上的值将其传递给所有方法。您需要密切关注它的使用情况是否正常,以及它是否没有以过高的速率分配。

【讨论】:

    【解决方案3】:

    我很同情您对旧库 slimdx 的问题,它可能不兼容 .NET。这种情况我处理过。

    建议:

    1. 使用性能效率更高的通用列表或数组,如 ArrayList。它会跟踪数组的大小,因此您不必这样做。一次分配列表,块,例如一次 100 个元素。
    2. 使用 C++ .NET 并利用不安全数组或 .NET 类(如 ArrayList)。
    3. 更新:使用虚拟内存的思想。将一些数据保存到 XML 文件或 SQL 数据库中,从而释放大量内存。

    我知道这无论如何都是一场赌博。

    【讨论】:

    • 我不能使用自动调整大小的通用 .net 类。调整大小后,数据将在我不知道的情况下复制到新的内存地址,而 slimdx 仍将指向旧位置。我看不出 c++ .net 对此有何帮助。
    • 使用 C++,您的代码可以使用不安全的类型和指针,并且您可以将地址传递给 slimdx 函数。但是你仍然需要自己管理内存。
    • 使用虚拟内存的想法,正如我在上面的建议列表中添加的那样。我知道这会添加代码,但它不是软件重新设计。
    • 我真的不能为此使用虚拟内存。渲染需要所有数据,因此必须保留在内存中。玩家视野之外的任何东西现在都被卸载了,所以它不像视野外的区域正在使用任何内存。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-05-21
    • 1970-01-01
    • 2022-09-27
    • 1970-01-01
    相关资源
    最近更新 更多