在C#中查找对象实例的大小(以字节为单位)

141

对于任意实例(包括不同对象的集合,组合,单个对象等),如何确定其占用的字节数?

(我目前有一个包含各种对象的集合,并尝试确定它们的聚合大小)

编辑:是否有人编写了一种可为Object编写的扩展方法来完成此操作?我认为那会很不错。


3
可能重复:使用C#获取字段大小(以字节为单位) - AxelEckenberger
17个回答

63

首先,警告一下:接下来的内容严格属于丑陋、未记录的黑科技领域。不要依赖这个方法的可行性——即使现在它能用,明天随着任何微小或重大的.NET更新,都有可能无法使用。

您可以使用本文介绍的CLR内部信息《MSDN杂志》2005年5月刊 - 深入探究.NET框架内部以了解CLR如何创建运行时对象 - 我上次检查时,它仍然适用。以下是此操作的实现方式(通过类型的TypeHandle检索内部的“基本实例大小”字段)。

object obj = new List<int>(); // whatever you want to get the size of
RuntimeTypeHandle th = obj.GetType().TypeHandle;
int size = *(*(int**)&th + 1);
Console.WriteLine(size);

这适用于 3.5 SP1 32 位。如果在 64 位上字段大小不同,您可能需要调整类型和/或偏移量。

此方法适用于所有“常规”类型,对于这些类型的所有实例来说,它们都具有相同且明确定义的类型。其中不适用的是数组和字符串,对于它们,您将不得不将所有包含元素的大小添加到其基本实例大小中,我认为 StringBuilder 也是如此。


3
这个是否只适用于托管 C++,还是可以在 C# 上运行?我尝试在 C# 中使用它时出现问题:无法获取托管类型('System.RuntimeTypeHandle')的地址、大小或声明指针。 - Maslow
23
甚至不需要使用不安全代码,.NET 4 版本就可以做到:Marshal.ReadInt32(type.TypeHandle.Value, 4) 对于 x86 和 x64 都适用。我只测试了结构体和类类型。请记住,这将返回值类型的 装箱 大小。@Pavel也许你可以更新你的回答。 - jnm2
2
@sab669 好的,在他的示例中用 obj.GetType() 替换 type。无论你使用哪个框架,只要看 CLR(v2 或 v4 或 CoreCLR)。我还没有在 CoreCLR 上尝试过这个。 - jnm2
1
这个回答是否真正符合问题的意图?我测试了@jnm2的代码,它只返回Type的大小(即所有引用句柄的大小总和,以及可能还包括指向方法的指针的大小?),但它不包括每个对象字段中数据的大小。我认为OP想知道的是对象的聚合大小,包括根拥有的子对象的大小? - Sam Goldberg
2
@SamGoldberg 手动计算是非常麻烦的,还有许多边缘情况需要考虑。Sizeof 告诉您对象的静态大小,而不是运行时对象图的内存消耗。VS2017 的内存和 CPU 分析工具非常好用,ReSharper 和其他工具也是如此,这就是我用来测量的工具。 - jnm2
显示剩余8条评论

22

虽然并不直接回答问题,但对于那些有兴趣在调试时研究对象大小的人:

  1. 在VS中开始调试,确保显示了诊断工具窗口(调试> 窗口> 显示诊断工具)
  2. 设置断点(可选)
  3. 在暂停时单击获取快照以查看内存使用情况
  4. 查看快照(可选择按字母顺序对对象列表进行排序以查找您感兴趣的类型)

输入图像描述


20

如果您正在处理可序列化的对象,可以尝试使用二进制序列化器模拟将其序列化(但将输出路由到虚无)的方式来近似估算大小。

class Program
{
    static void Main(string[] args)
    {
        A parent;
        parent = new A(1, "Mike");
        parent.AddChild("Greg");
        parent.AddChild("Peter");
        parent.AddChild("Bobby");

        System.Runtime.Serialization.Formatters.Binary.BinaryFormatter bf =
           new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter();
        SerializationSizer ss = new SerializationSizer();
        bf.Serialize(ss, parent);
        Console.WriteLine("Size of serialized object is {0}", ss.Length);
    }
}

[Serializable()]
class A
{
    int id;
    string name;
    List<B> children;
    public A(int id, string name)
    {
        this.id = id;
        this.name = name;
        children = new List<B>();
    }

    public B AddChild(string name)
    {
        B newItem = new B(this, name);
        children.Add(newItem);
        return newItem;
    }
}

[Serializable()]
class B
{
    A parent;
    string name;
    public B(A parent, string name)
    {
        this.parent = parent;
        this.name = name;
    }
}

class SerializationSizer : System.IO.Stream
{
    private int totalSize;
    public override void Write(byte[] buffer, int offset, int count)
    {
        this.totalSize += count;
    }

    public override bool CanRead
    {
        get { return false; }
    }

    public override bool CanSeek
    {
        get { return false; }
    }

    public override bool CanWrite
    {
        get { return true; }
    }

    public override void Flush()
    {
        // Nothing to do
    }

    public override long Length
    {
        get { return totalSize; }
    }

    public override long Position
    {
        get
        {
            throw new NotImplementedException();
        }
        set
        {
            throw new NotImplementedException();
        }
    }

    public override int Read(byte[] buffer, int offset, int count)
    {
        throw new NotImplementedException();
    }

    public override long Seek(long offset, System.IO.SeekOrigin origin)
    {
        throw new NotImplementedException();
    }

    public override void SetLength(long value)
    {
        throw new NotImplementedException();
    }
}

7
当然,这可以为你提供一个最小的大小,但并不能告诉你它在内存中的大小。 - John Saunders
哈哈,我回来之前用的是二进制序列化器。John,这样做不会给你实际的内存大小吗? - Janie
2
它将给出序列化大小,这将是序列化器所需的大小,用于“序列化器”目的。这些大小可能与“内存中”的大小不同。例如,序列化程序可能会在三个字节中存储较小的整数。 - John Saunders
5
正如我所说的,这只是一个近似值。它并不完美,但我不同意它告诉你关于内存大小“什么都没有”的说法。我认为它给出了一些想法——更大的序列化通常与更大的内存大小相关联。存在一定的关系。 - BlueMonkMN
我同意 - 获取 .NET 对象图的大致大小估计是有用的。 - Craig Shearer

13

针对未托管类型(也称值类型)、结构体:

Marshal.SizeOf(object);

对于托管对象,我所能得到的最接近的结果是一个近似值。

long start_mem = GC.GetTotalMemory(true);

aclass[] array = new aclass[1000000];
for (int n = 0; n < 1000000; n++)
    array[n] = new aclass();

double used_mem_median = (GC.GetTotalMemory(false) - start_mem)/1000000D;

请不要使用序列化。二进制格式添加了头部,因此您可以更改类并将旧的序列化文件加载到修改后的类中。

此外,它不会告诉您在内存中的实际大小,也不会考虑内存对齐。

[编辑] 通过对类的每个属性递归地使用BitConverter.GetBytes(prop-value),您将获得以字节为单位的内容,这不计算类或引用的权重,但更接近现实。 如果大小很重要,我建议使用字节数组来存储数据,并使用非托管代理类使用指针转换访问值,注意这将是非对齐的内存,因此在旧计算机上速度会很慢,但在现代RAM上处理巨大数据集时将快得多,因为将从RAM读取的大小最小化将产生更大的影响。


7

安全的解决方案,同时进行了一些优化 CyberSaving/MemoryUsage代码。 一些案例:

/* test nullable type */      
TestSize<int?>.SizeOf(null) //-> 4 B

/* test StringBuilder */    
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 100; i++) sb.Append("わたしわたしわたしわ");
TestSize<StringBuilder>.SizeOf(sb ) //-> 3132 B

/* test Simple array */    
TestSize<int[]>.SizeOf(new int[100]); //-> 400 B

/* test Empty List<int>*/    
var list = new List<int>();  
TestSize<List<int>>.SizeOf(list); //-> 205 B

/* test List<int> with 100 items*/
for (int i = 0; i < 100; i++) list.Add(i);
TestSize<List<int>>.SizeOf(list); //-> 717 B

它也适用于类:

class twostring
{
    public string a { get; set; }
    public string b { get; set; }
}
TestSize<twostring>.SizeOf(new twostring() { a="0123456789", b="0123456789" } //-> 28 B

这也是我会采取的方法。您可以将此前遇到的对象集合添加到图形中,以避免a)无限递归和b)避免重复添加相同的内存。 - mafu
这应该是被接受的答案。稍微复杂一些,但是正确的。 - undefined
代码需要进行一处小修复,以便在DateTime和DateTimeOffset中返回8。 - undefined

5
这不适用于当前的.NET实现,但需要记住的一件事是,在垃圾回收/托管运行时中,对象的分配大小可以在程序的生命周期内发生变化。例如,某些分代垃圾收集器(如分代/后继引用计数混合收集器)只需要在对象从幼年空间移动到成熟空间后存储某些信息。

这使得创建可靠的通用API来公开对象大小变得不可能。

有趣。那么人们如何动态确定对象/对象集合的大小呢? - Janie
2
这取决于他们需要它做什么。如果是用于P/Invoke(本机代码互操作),则使用Marshal.SizeOf(typeof(T))。如果是用于内存分析,则使用与执行环境合作提供信息的单独分析器。如果您对数组中元素的对齐方式感兴趣,可以在DynamicMethod中使用SizeOf IL操作码(我认为在.NET框架中没有更简单的方法)。 - Sam Harwell

4

在运行时做到这一点是不可能的。

有各种内存分析工具可以显示对象大小。

编辑:您可以编写一个使用CLR Profiling API对第一个程序进行性能分析并通过远程控制或其他方式与其通信的第二个程序。


22
如果在运行时无法完成,那么内存分析器是如何提供信息的? - Janie
2
通过使用性能分析API。但是,一个程序不能对自己进行分析。 - SLaks
有趣。如果我想让代码处理对象消耗过多内存的情况怎么办? - Janie
4
那么,你将要处理有自我意识的软件,我会非常害怕。 :-) 开玩笑的,"单一职责原则" - 让程序做它该做的事情,让其他代码片段监测是否有对象占用太多内存。 - John Saunders
2
@Janie:你也会对大小的重要性以及它与性能的关系做出假设。我认为在这之前,你需要成为一个真正的低级CLR性能专家(那种已经了解了Profiling API的人)。否则,你可能会将早期的经验应用到不适用的情况中。 - John Saunders

3

如果有人正在寻找一种不需要使用[Serializable]类且结果是近似而非精确科学的解决方案。

我找到的最佳方法是使用UTF32编码将json序列化为内存流。

private static long? GetSizeOfObjectInBytes(object item)
{
    if (item == null) return 0;
    try
    {
        // hackish solution to get an approximation of the size
        var jsonSerializerSettings = new JsonSerializerSettings
        {
            DateFormatHandling = DateFormatHandling.IsoDateFormat,
            DateTimeZoneHandling = DateTimeZoneHandling.Utc,
            MaxDepth = 10,
            ReferenceLoopHandling = ReferenceLoopHandling.Ignore
        };
        var formatter = new JsonMediaTypeFormatter { SerializerSettings = jsonSerializerSettings };
        using (var stream = new MemoryStream()) { 
            formatter.WriteToStream(item.GetType(), item, stream, Encoding.UTF32);
            return stream.Length / 4; // 32 bits per character = 4 bytes per character
        }
    }
    catch (Exception)
    {
        return null;
    }
}

不,这不会给出在内存中使用的精确大小。正如先前提到的那样,这是不可能的。但它会给你一个大概的估计。

请注意,这也相当慢。


3

2

对于结构体/值的数组,我使用以下方法得到不同的结果:

first = Marshal.UnsafeAddrOfPinnedArrayElement(array, 0).ToInt64();
second = Marshal.UnsafeAddrOfPinnedArrayElement(array, 1).ToInt64();
arrayElementSize = second - first;
< p >(简化的例子)

无论采用何种方法,您都需要了解 .Net 的工作原理才能正确解释结果。例如,返回的元素大小是“对齐”元素大小,并带有一些填充。根据类型的使用情况,“包装”在 GC 堆上,堆栈上,作为字段或作为数组元素时,开销和大小是不同的。

(我想知道使用“虚拟”空结构体(没有任何字段)来模拟泛型的“可选”参数会产生什么记忆影响;通过使用涉及空结构体的不同布局进行测试,我可以看到每个空结构体使用(至少)1字节的内存;我模糊地记得这是因为 .Net 需要为每个字段提供不同的地址,如果字段真的为空/大小为0,则无法正常工作)。


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