C#内存开销从何而来
C# Where does the memory overhead come from
我的资源有点问题。 .NET 似乎正在产生大量的内存开销 and/or 并没有释放它不应该需要的内存。但是问题来了:
我有一个读取以下内容的 STL 文件的对象 class:
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。添加到 triangleList
的 class cSTLTriangle
如下:
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;
}
}
class 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);
}
}
如果我计算一下我的 classes 中使用的类型的大小,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;
}
在下一个记录点,事情变得荒谬了。在日志点 4 和日志点 3 之间,内存使用量增加了大约原始 STL 文件大小的 4.73 倍(如您所见,我在解析每个三角形时大量使用 .SubArray
)。
当我让程序继续运行时,内存使用量没有显着增加:好,但也根本没有减少:坏。我希望保存文件的 byte[]
释放内存,因为它超出了范围,就像我传递给 ReadTriangles(byte[] ...)
的子数组一样,但不知何故它们没有。 我最终得到的 "overhead" 是原始 STL 数据大小的 5.7 倍。
这是正常行为吗? .NET 运行时是否会像 Photoshop 那样在获得一些有用的 RAM 后保持内存分配(即使它已扩展到磁盘)?如何减少这种 classes 组合的内存占用?
编辑:
- 问题发生在 Win7 企业版 x64 上。 .NET 4.5,为 x64(还有 AnyCPU)编译
- 我在对象创建完成后调用了
GC.Collect()
(因此在对象本身之外),但没有任何反应。只有在将对象引用设置为 null
后,我才找回内存
在日志点 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
}
您可以尝试一些方法来减少内存使用量。
首先,如果可能的话,您应该重写您的文件加载代码,以便它只加载它需要的数据,而不是一次加载整个文件。
例如,您可以将 header 作为单个块读取,然后将每个三角形的数据作为单个块读取(在循环中)。
其次,您的大 object 堆可能存在碎片 - 垃圾收集器不会移动大 object 堆,因此无法对其进行碎片整理。 (如果 .Net 4.51 修复了此问题,但您必须显式启用大型 object 堆碎片整理,并显式启动它。)
您可以通过 pre-sizing 您的 triangleList
.
来缓解这个问题
目前,您依次将每个三角形添加到 triangleList
,从容量为零的列表开始。这意味着每隔一段时间就会超出列表的容量,导致列表被扩展。
当列表已满时通过向其中添加项目来扩展列表时,它:
- 创建一个两倍于当前缓冲区大小的新内部缓冲区。
- 将旧缓冲区复制到新缓冲区。
- 删除旧缓冲区。
- 将新项目复制到新缓冲区。
问题是双重的:
- 正在进行大量冗余复制。
- 如果内部缓冲区超过将 objects 放在大 object 堆上的阈值,您可能会出现堆碎片。
由于您事先知道三角形列表的最大大小,因此您可以通过在向其中添加项目之前设置列表的容量来解决此问题:
triangleList.Capacity = numberOfTriangles;
内存开销
您的 cVector
class 增加了 很多 内存开销。在 32 位系统上,任何引用对象都有 12 字节 的内存开销(尽管如果可能的话,其中 4 个可以免费供字段使用),如果我没记错的话。让我们考虑 8 个字节的开销。因此,在您的情况下,有 10,000,000 个三角形,每个三角形包含 4 个向量,加起来为:
10,000,000 * 4 * 8 = 305 MB of overhead
如果您 运行 在 64 位系统上,它是两倍:
10,000,000 * 4 * 16 = 610 MB of overhead
除此之外,您还有四个引用的开销,每个引用 cSTLTriangle
都必须指向向量,因此:
10,000,000 * 4 * 4 = 152 MB (32-bit)
10,000,000 * 4 * 8 = 305 MB (64-bit)
如您所见,这一切都会产生相当大的开销。
因此,在这种情况下,我建议您将 cVector
设为 struct
。正如评论中所讨论的,结构可以实现接口(以及属性和方法)。请注意@Jcl 提到的caveats。
您的 cSTLTriangle
class 也有同样的问题(32 位和 64 位的开销分别约为 76/152 MB),尽管我不确定它的大小我想推荐在这上面使用 struct。这里的其他人可能对此事有有用的见解。
此外,由于填充和对象布局,开销实际上可能更大,但我没有在这里考虑到这一点。
列表容量
将 List<T>
class 与该数量的对象一起使用会导致一些内存浪费。正如@Matthew Watson 提到的,当列表的内部数组没有更多空间时,它将被扩展。事实上,每次发生这种情况时,它的容量都会增加一倍。在对 10533050 个条目的测试中,列表的容量最终为 16777216 个条目,开销为:
( 16777216 - 10533050 ) * 4 byte reference = 23 MB (32-bit)
( 16777216 - 10533050 ) * 8 byte reference = 47 MB (64-bit)
既然你事先知道三角形的数量,我建议只使用一个简单的数组。手动设置列表的 Capacity
也可以。
其他问题
评论中讨论的其他问题不应该给你任何内存开销,但它们肯定会给 GC 带来很多不必要的压力。特别是 SubArray
方法,虽然非常实用,但会创建数百万个垃圾数组供 GC 处理。我建议跳过它并手动索引到数组中,即使它需要更多工作。
另一个问题是一次读取整个文件。与逐条读取相比,这将既慢又占用更多内存。由于您需要处理的字节序问题,可能无法像其他人建议的那样直接使用 BinaryReader
。一个复杂的选项 可能 是使用内存映射文件,这样您就可以访问数据而不必关心它是否已被读取,将细节留给 OS .
(伙计,我希望我把所有这些数字都弄对了)
我的资源有点问题。 .NET 似乎正在产生大量的内存开销 and/or 并没有释放它不应该需要的内存。但是问题来了:
我有一个读取以下内容的 STL 文件的对象 class:
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。添加到 triangleList
的 class cSTLTriangle
如下:
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;
}
}
class 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);
}
}
如果我计算一下我的 classes 中使用的类型的大小,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;
}
在下一个记录点,事情变得荒谬了。在日志点 4 和日志点 3 之间,内存使用量增加了大约原始 STL 文件大小的 4.73 倍(如您所见,我在解析每个三角形时大量使用 .SubArray
)。
当我让程序继续运行时,内存使用量没有显着增加:好,但也根本没有减少:坏。我希望保存文件的 byte[]
释放内存,因为它超出了范围,就像我传递给 ReadTriangles(byte[] ...)
的子数组一样,但不知何故它们没有。 我最终得到的 "overhead" 是原始 STL 数据大小的 5.7 倍。
这是正常行为吗? .NET 运行时是否会像 Photoshop 那样在获得一些有用的 RAM 后保持内存分配(即使它已扩展到磁盘)?如何减少这种 classes 组合的内存占用?
编辑:
- 问题发生在 Win7 企业版 x64 上。 .NET 4.5,为 x64(还有 AnyCPU)编译
- 我在对象创建完成后调用了
GC.Collect()
(因此在对象本身之外),但没有任何反应。只有在将对象引用设置为null
后,我才找回内存
在日志点 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
}
您可以尝试一些方法来减少内存使用量。
首先,如果可能的话,您应该重写您的文件加载代码,以便它只加载它需要的数据,而不是一次加载整个文件。
例如,您可以将 header 作为单个块读取,然后将每个三角形的数据作为单个块读取(在循环中)。
其次,您的大 object 堆可能存在碎片 - 垃圾收集器不会移动大 object 堆,因此无法对其进行碎片整理。 (如果 .Net 4.51 修复了此问题,但您必须显式启用大型 object 堆碎片整理,并显式启动它。)
您可以通过 pre-sizing 您的 triangleList
.
目前,您依次将每个三角形添加到 triangleList
,从容量为零的列表开始。这意味着每隔一段时间就会超出列表的容量,导致列表被扩展。
当列表已满时通过向其中添加项目来扩展列表时,它:
- 创建一个两倍于当前缓冲区大小的新内部缓冲区。
- 将旧缓冲区复制到新缓冲区。
- 删除旧缓冲区。
- 将新项目复制到新缓冲区。
问题是双重的:
- 正在进行大量冗余复制。
- 如果内部缓冲区超过将 objects 放在大 object 堆上的阈值,您可能会出现堆碎片。
由于您事先知道三角形列表的最大大小,因此您可以通过在向其中添加项目之前设置列表的容量来解决此问题:
triangleList.Capacity = numberOfTriangles;
内存开销
您的 cVector
class 增加了 很多 内存开销。在 32 位系统上,任何引用对象都有 12 字节 的内存开销(尽管如果可能的话,其中 4 个可以免费供字段使用),如果我没记错的话。让我们考虑 8 个字节的开销。因此,在您的情况下,有 10,000,000 个三角形,每个三角形包含 4 个向量,加起来为:
10,000,000 * 4 * 8 = 305 MB of overhead
如果您 运行 在 64 位系统上,它是两倍:
10,000,000 * 4 * 16 = 610 MB of overhead
除此之外,您还有四个引用的开销,每个引用 cSTLTriangle
都必须指向向量,因此:
10,000,000 * 4 * 4 = 152 MB (32-bit)
10,000,000 * 4 * 8 = 305 MB (64-bit)
如您所见,这一切都会产生相当大的开销。
因此,在这种情况下,我建议您将 cVector
设为 struct
。正如评论中所讨论的,结构可以实现接口(以及属性和方法)。请注意@Jcl 提到的caveats。
您的 cSTLTriangle
class 也有同样的问题(32 位和 64 位的开销分别约为 76/152 MB),尽管我不确定它的大小我想推荐在这上面使用 struct。这里的其他人可能对此事有有用的见解。
此外,由于填充和对象布局,开销实际上可能更大,但我没有在这里考虑到这一点。
列表容量
将 List<T>
class 与该数量的对象一起使用会导致一些内存浪费。正如@Matthew Watson 提到的,当列表的内部数组没有更多空间时,它将被扩展。事实上,每次发生这种情况时,它的容量都会增加一倍。在对 10533050 个条目的测试中,列表的容量最终为 16777216 个条目,开销为:
( 16777216 - 10533050 ) * 4 byte reference = 23 MB (32-bit)
( 16777216 - 10533050 ) * 8 byte reference = 47 MB (64-bit)
既然你事先知道三角形的数量,我建议只使用一个简单的数组。手动设置列表的 Capacity
也可以。
其他问题
评论中讨论的其他问题不应该给你任何内存开销,但它们肯定会给 GC 带来很多不必要的压力。特别是 SubArray
方法,虽然非常实用,但会创建数百万个垃圾数组供 GC 处理。我建议跳过它并手动索引到数组中,即使它需要更多工作。
另一个问题是一次读取整个文件。与逐条读取相比,这将既慢又占用更多内存。由于您需要处理的字节序问题,可能无法像其他人建议的那样直接使用 BinaryReader
。一个复杂的选项 可能 是使用内存映射文件,这样您就可以访问数据而不必关心它是否已被读取,将细节留给 OS .
(伙计,我希望我把所有这些数字都弄对了)