C#内存开销来自哪里

时间:2015-01-16 12:45:05

标签: c# memory

我在这里遇到一点资源问题。似乎.NET正在创建大量的内存开销和/或不释放它不应该需要的内存。但问题是:

我有一个对象,它读取以下类的STL文件:

public class cSTLBinaryDataModel
{
    public byte[] header { get; private set; }
    public UInt32 triangleCount { get { return Convert.ToUInt32(triangleList.Count); } }
    public List<cSTLTriangle> triangleList { get; private set; }

    public  cSTLBinaryDataModel()
    {
        header = new byte[80];
        triangleList = new List<cSTLTriangle>();
    }

    public void ReadFromFile(string in_filePath)
    {
        byte[] stlBytes;
//Memory logpoint 1
        stlBytes = File.ReadAllBytes(in_filePath);
//Memory logpoint 2
        ReadHeader(stlBytes.SubArray(0, cConstants.BYTES_IN_HEADER));
        ReadTriangles(stlBytes.SubArray(cConstants.BYTES_IN_HEADER, stlBytes.Length - cConstants.BYTES_IN_HEADER));
//Evaluate memory logpoints here
    }

    private void ReadHeader(byte[] in_header)
    {
        header = in_header;
    }

    private void ReadTriangles(byte[] in_triangles)
    {
        UInt32 numberOfTriangles = BitConverter.ToUInt32(cHelpers.HandleLSBFirst(in_triangles.SubArray(0, 4)), 0);
//Memory logpoint 3
        for (UInt32 i = 0; i < numberOfTriangles; i++)
        {
            triangleList.Add(new cSTLTriangle(in_triangles.SubArray(Convert.ToInt32(i * cConstants.BYTES_PER_TRIANGLE + 4), Convert.ToInt32(cConstants.BYTES_PER_TRIANGLE))));
        }
//Memory logpoint 4
    }
}

我的STL文件很大(但可以变得更大);它包含10533050个三角形,因此在磁盘上大小约为520 MB。添加到cSTLTriangle的课程triangleList如下:

public class cSTLTriangle
{
    public cVector normalVector { get; private set; }
    public cVector[] vertices { get; private set; }
    public UInt16 attributeByteCount { get; private set; }
    public bool triangleFilledWithExternalValues { get; private set; }

    public cSTLTriangle(byte[] in_bytes)
    {
        Initialize();
        normalVector = new cVector(BitConverter.ToSingle(cHelpers.HandleLSBFirst(in_bytes.SubArray(0, 4)), 0),
            BitConverter.ToSingle(cHelpers.HandleLSBFirst(in_bytes.SubArray(4, 4)), 0),
            BitConverter.ToSingle(cHelpers.HandleLSBFirst(in_bytes.SubArray(8, 4)), 0));
        vertices[0] = new cVector(BitConverter.ToSingle(cHelpers.HandleLSBFirst(in_bytes.SubArray(12, 4)), 0),
            BitConverter.ToSingle(cHelpers.HandleLSBFirst(in_bytes.SubArray(16, 4)), 0),
            BitConverter.ToSingle(cHelpers.HandleLSBFirst(in_bytes.SubArray(20, 4)), 0));
        vertices[1] = new cVector(BitConverter.ToSingle(cHelpers.HandleLSBFirst(in_bytes.SubArray(24, 4)), 0),
            BitConverter.ToSingle(cHelpers.HandleLSBFirst(in_bytes.SubArray(28, 4)), 0),
            BitConverter.ToSingle(cHelpers.HandleLSBFirst(in_bytes.SubArray(32, 4)), 0));
        vertices[2] = new cVector(BitConverter.ToSingle(cHelpers.HandleLSBFirst(in_bytes.SubArray(36, 4)), 0),
            BitConverter.ToSingle(cHelpers.HandleLSBFirst(in_bytes.SubArray(40, 4)), 0),
            BitConverter.ToSingle(cHelpers.HandleLSBFirst(in_bytes.SubArray(44, 4)), 0));
        attributeByteCount = BitConverter.ToUInt16(cHelpers.HandleLSBFirst(in_bytes.SubArray(48, 2)), 0);
        triangleFilledWithExternalValues = true;
    }

    public cSTLTriangle(cVector in_vertex1, cVector in_vertex2, cVector in_vertex3)
    {
        Initialize();
        vertices[0] = in_vertex1;
        vertices[1] = in_vertex2;
        vertices[2] = in_vertex3;
        normalVector = cVectorOperations.CrossProduct(cVectorOperations.GetDirectionVector(vertices[0], vertices[1]), cVectorOperations.GetDirectionVector(vertices[0], vertices[2]));
    }
    /// <summary>
    /// Resets object to a defined state
    /// </summary>
    private void Initialize()
    {
        vertices = new cVector[3];
        //from here on not strictly necessary, but it helps with resetting the object after an error
        normalVector = new cVector(0, 0, 0);
        vertices[0] = new cVector(0, 0, 0);
        vertices[1] = new cVector(0, 0, 0);
        vertices[2] = new cVector(0, 0, 0);
        attributeByteCount = 0;
        triangleFilledWithExternalValues = false;
    }
}

课程cVector是:(抱歉这么多代码)

public class cVector:ICloneable
{
    public float component1 { get; set; }
    public float component2 { get; set; }
    public float component3 { get; set; }
    public double Length { get { return Math.Sqrt(Math.Pow(component1, 2) + Math.Pow(component2, 2) + Math.Pow(component3, 2)); } }

    public cVector(float in_value1, float in_value2, float in_value3)
    {
        component1 = in_value1;
        component2 = in_value2;
        component3 = in_value3;
    }

    public object Clone()
    {
        return new cVector(component1, component2, component3);
    }
}

如果我计算我的类中使用的类型的大小,则cSTLTriangle的一个实例的数量为51个字节。我知道必须有一个开销来容纳功能等。但是,如果我将此大小乘以三角形的数量,我最终会达到512,3 MB,这与实际文件大小非常一致。我会想象triangleList占用大致相同的内存量(再次允许轻微的开销,但仍然是List<T>),但不是! (使用GC.GetTotalMemory(false)来评估内存)

从Logpoint 1到Logpoint 2,增加了526660800字节,这非常准确地是加载到字节数组中的STL文件的大小。 在Logpoint 3和Logpoint 2之间增加了大致相同的数量,这是可以理解的,因为我将一个子数组传递给ReadTriangles方法。 SubArray是我在SO上找到的代码(这可能是伪装的恶魔吗?):

public static T[] SubArray<T>(this T[] data, int index, int length)
{
    T[] result = new T[length];
    Array.Copy(data, index, result, 0, length);
    return result;
}

下一个Logpoint的事情变得荒谬可笑。在Logpoint 4和Logpoint 3之间,内存使用量大约是原始STL文件大小的4.73倍(正如您所看到的,我在解析每个三角形时大量使用.SubArray。)

当我让程序继续运行时,内存使用量没有显着增加:好,但也没有减少:糟糕。我希望保持文件的byte[]释放内存,因为它超出范围,我传递给ReadTriangles(byte[] ...)的子数组也是如此,但不知何故他们没有。 我的最终结果是原始STL数据大小的5.7倍。

这是通常的行为吗? .NET运行时是否保持内存分配(即使它已经扩展到磁盘),就像Photoshop一样,一旦它获得了一些jucy RAM?如何减少这种类组合的内存占用?

编辑:

  • Win7 Enterprise,x64出现问题。 .NET 4.5,为x64编译(但也是AnyCPU)
  • 我在对象创建完成后调用了GC.Collect()(所以在对象本身之外)并没有发生任何事情。只有在将对象引用设置为null后才能恢复内存

3 个答案:

答案 0 :(得分:3)

内存开销

您的cVector类添加很多的内存开销。在32位系统上,任何参考对象的内存开销 12字节(尽管如果可能,其中4个可以由字段自由使用),如果我没记错的话。让我们用8字节的开销。因此,在您的情况下,10,000,000个三角形,每个包含4个向量,将upp添加到:

  

10,000,000 * 4 * 8 = 305 MB的开销

如果你在64位系统上运行,那就是它的两倍:

  

10,000,000 * 4 * 16 = 610 MB的开销

除此之外,你还有四个引用的开销,每个cSTLTriangle将对向量进行引用,为您提供:

  

10,000,000 * 4 * 4 = 152 MB(32位)

     

10,000,000 * 4 * 8 = 305 MB(64位)

正如你所看到的,所有这些都会产生相当大的开销。

因此,在这种情况下,我建议您cVectorstruct。正如评论中所讨论的,结构可以实现接口(以及属性和方法)。请注意@Jcl提到的caveats

你的cSTLTriangle类有同样的问题(32位和64位分别约为76/152 MB开销),虽然它的大小我不确定我是否想要推荐跟那个结构一起去。这里的其他人可能对这个问题有有用的见解。

此外,由于填充和对象布局,开销可能实际上更大,但我还没有考虑到这一点。

列出容量

List<T>类与该数量的对象一起使用可能会导致内存浪费。正如@Matthew Watson所提到的,当列表的内部数组没有更多空间时,它将被扩展。事实上,每次发生这种情况时,它的容量都会增加一倍。在使用您的10533050个条目的测试中,列表的容量最终为16777216个条目,产生了以下开销:

  

(16777216 - 10533050)* 4字节参考= 23 MB(32位)

     

(16777216 - 10533050)* 8字节参考= 47 MB​​(64位)

因为你事先知道了三角形的数量,所以我建议你选择一个简单的数组。手动设置列表的Capacity也可以。

其他问题

评论中讨论的其他问题不应该给你任何内存开销,但他们肯定会给GC带来很多不必要的压力。尤其是SubArray方法,虽然非常实用,但会为GC创建数百万个垃圾数组来处理。我建议跳过它并手动索引到数组中,即使它的工作量更大。

另一个问题是立刻读取整个文件。这将比较慢并且使用更多的内存而不是逐个读取它。由于您需要处理的字节序问题,可能无法直接使用其他人提出的BinaryReader。一个复杂的选项可以使用内存映射文件,这样可以让您访问数据而无需关心它是否被读取,将详细信息留给操作系统。

(男人,我希望我得到所有这些数字)

答案 1 :(得分:2)

您可以尝试减少内存使用量。

首先,如果可能,您应该重写文件加载代码,以便它只加载所需的数据,而不是一次加载整个文件。

例如,您可以将标题读取为单个块,然后将每个三角形的数据作为单个块读取(在循环中)。

其次,您的大型对象堆可能存在碎片 - 垃圾收集器不会移动大型对象,因此无法对其进行碎片整理。 (如果为.Net 4.51修复了此问题,但您必须显式启用大对象堆碎片整理,并明确地发起它。)

您可以通过预先调整triangleList

的大小来缓解此问题

目前,您依次将每个三角形添加到triangleList,从容量为零的列表开始。这意味着每隔一段时间就会超出列表的容量,导致它被扩展。

当列表在容量时通过向其添加项目来扩展列表时,它:

  • 创建一个两倍于当前缓冲区大小的新内部缓冲区。
  • 将旧缓冲区复制到新缓冲区。
  • 删除旧缓冲区。
  • 将新项目复制到新缓冲区。

问题出在哪里:

  1. 正在进行大量的冗余复制。
  2. 如果内部缓冲区超过了将对象放在大对象堆上的阈值,则可能会出现堆碎片。
  3. 由于您事先知道三角形列表的最大大小,您可以通过在向其添加项目之前设置列表的容量来解决此问题:

    triangleList.Capacity = numberOfTriangles;
    

答案 2 :(得分:0)

在logpoint 2之后,你可能会尝试将代码拆分一下,以便你有一个

byte[] header
byte[] triangles

并且一旦完成拆分原始字节数组,将其设置为null,然后您可以使用System.GC.Collect()强制垃圾收集器运行。这应该可以节省一些内存。

 public void ReadFromFile(string in_filePath)
    {
        byte[] stlBytes;
//Memory logpoint 1
        stlBytes = File.ReadAllBytes(in_filePath);
//Memory logpoint 2
        byte[] header = stlBytes.SubArray(0, cConstants.BYTES_IN_HEADER);
        byte[] triangles = stlBytes.SubArray(cConstants.BYTES_IN_HEADER, stlBytes.Length - cConstants.BYTES_IN_HEADER);
        ReadHeader(header);
        ReadTriangles(triangles);
        stlBytes = null;
        System.GC.Collect();
//Evaluate memory logpoints here
    }