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


114

对于任何任意实例(不同对象,组成,单个对象等的集合)

如何确定其大小(以字节为单位)?

(我目前有各种对象的集合,我正在尝试确定它的总大小)

编辑:有人编写了可以做到这一点的对象扩展方法吗?那真是整洁的imo。



Answers:


60

首先,要发出警告:接下来的事情完全是在丑陋的,未经记录的黑客领域。不要依赖于此工作-即使现在为您工作,它也可能会因任何次要或主要的.NET更新而在明天停止工作。

您可以在CLR内部构件上使用本文中的信息,MSDN杂志2005年5月-深入.NET Framework内部构件以查看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。对于它们,您需要将所有包含的元素的大小添加到其基本实例大小中。


否。没有“适当的”方法来执行此操作,因为首先它不是行为良好的.NET应用程序应关注的问题。上面的代码直接考虑了CLR 特定实现内部数据结构(例如,在下一版.NET中可能会轻易更改)。
帕维尔·米纳夫

3
应该在C#或仅托管c ++中工作吗?到目前为止,在C#中我尝试过它并不满意:Cannot take the address of, get the size of, or declare a pointer to a managed type ('System.RuntimeTypeHandle')
Maslow 2014年

17
.NET 4版本甚至不需要不安全的代码:Marshal.ReadInt32(type.TypeHandle.Value, 4)适用于x86和x64。我只测试了结构和类类型。请记住,这将返回值类型的装箱大小。@Pavel也许您可以更新您的答案。
jnm2

2
@ sab669好,在他的示例中替换typeobj.GetType()。使用哪个框架都无关紧要,只有什么CLR(v2或v4或CoreCLR)无关紧要。我没有在CoreCLR上尝试过。
jnm2'2

2
@SamGoldberg手动计算此结果需要处理一百万个边缘案例。Sizeof告诉您对象的静态大小,而不是对象的运行时图的内存消耗。VS2017的内存和CPU性能分析以及ReSharper的工具和其他工具都非常出色,这就是我要用来衡量的。
jnm2

21

如果您正在使用可序列化的对象,则可以通过假装使用二进制序列化器对其进行序列化(但将输出路由到遗忘)来估计大小。

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();
    }
}

6
当然,这可以为您提供最小的大小,但是不会告诉您有关内存大小的任何信息。
约翰·桑德斯

大声笑,我回来检查回复之前的下一个灯泡是使用二进制序列化器。约翰,这怎么会不给您实际的内存大小?
珍妮

2
它将为您提供序列化的大小,这将是序列化程序所需的大小,以实现“序列化”目的。这些可能与“坐在内存中”的目的不同。例如,也许序列化程序将较小的整数存储在三个字节中。
约翰·桑德斯

4
就像我说的,这只是一个近似值。它不是完美的,但是我不同意它告诉您“关于内存大小的任何信息”。我想说的是,它给了您一些想法-更大的序列化通常与更大的内存大小相关。有一些关系。
BlueMonkMN

我同意-获得.NET对象图大小的估算值很有用。
Craig Shearer

8

对于非托管类型(也称为值类型),结构:

        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;

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

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

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


5

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

这样就不可能创建可靠的通用API来暴露对象的大小。


有趣。那么人们如何动态地确定他们的对象/对象集合的大小?
珍妮

2
这取决于他们需要什么。如果是P / Invoke(本机代码互操作),则使用Marshal.SizeOf(typeof(T))。如果要进行内存分析,则它们将使用与执行环境配合使用的单独的探查器来提供信息。如果您对数组中的元素对齐感兴趣,则可以在DynamicMethod中使用SizeOf IL操作码(我认为.NET框架中没有更简便的方法)。
山姆·哈威尔2009年

5

经过优化的安全解决方案 Cyber​​Saving / 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)避免两次添加相同的内存。
马夫

4

这在运行时是不可能的。

但是,有多种内存分析器可以显示对象的大小。

编辑:您可以编写第二个程序,该文件使用CLR分析API对第一个程序进行概要分析,并通过远程处理或其他方式与之通信。


17
如果在运行时无法执行,则内存分析器如何提供信息?
珍妮

2
通过使用性能分析API。但是,程序无法自我
描述-SLaks

有趣。如果我想让代码处理对象消耗过多内存的情况该怎么办?
珍妮

4
然后您将要使用自我意识的软件,我会非常害怕。:-)认真地讲,“单一责任主体”-让程序成为程序,让其他代码监视对象占用过多内存。
约翰·桑德斯

2
@Janie:您还将假设大小的重要性以及它与性能之间的关系。我认为您应该先成为真正的低级CLR性能专家(这种人已经了解Profiling API)。否则,您可能会将您的早期经验应用于不适用的情况。
约翰·桑德斯


2

AFAIK,您不能不实际计算每个成员的大小(以字节为单位)。但是,成员的大小(像集合中的元素一样)是否计入对象的大小,或者指向该成员的指针是否计入对象的大小?取决于您如何定义它。

我遇到过这种情况,在此之前,我想根据消耗的内存来限制缓存中的对象。

好吧,如果有一些技巧可以做到这一点,我将很高兴知道这一点!



1

您可以使用反射来收集所有公共成员或属性信息(根据对象的类型)。但是,如果不遍历对象上的每个数据,就无法确定大小。


1

对于任何寻求不需要[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;
    }
}

不,这不会给您确切的内存大小。如前所述,这是不可能的。但这会给您一个粗略的估计。

请注意,这也相当慢。


1

从Pavel和jnm2:

private int DumpApproximateObjectSize(object toWeight)
{
   return Marshal.ReadInt32(toWeight.GetType().TypeHandle.Value, 4);
}

另外,请注意,因为它仅适用于连续的内存对象


1

我为.NET中的不同集合创建了基准测试: https //github.com/scholtz/TestDotNetCollectionsMemoryAllocation

对于具有1,000,000个对象并分配了3个属性的.NET Core 2.2,结果如下:

Testing with string: 1234567
Hashtable<TestObject>:                                     184 672 704 B
Hashtable<TestObjectRef>:                                  136 668 560 B
Dictionary<int, TestObject>:                               171 448 160 B
Dictionary<int, TestObjectRef>:                            123 445 472 B
ConcurrentDictionary<int, TestObject>:                     200 020 440 B
ConcurrentDictionary<int, TestObjectRef>:                  152 026 208 B
HashSet<TestObject>:                                       149 893 216 B
HashSet<TestObjectRef>:                                    101 894 384 B
ConcurrentBag<TestObject>:                                 112 783 256 B
ConcurrentBag<TestObjectRef>:                               64 777 632 B
Queue<TestObject>:                                         112 777 736 B
Queue<TestObjectRef>:                                       64 780 680 B
ConcurrentQueue<TestObject>:                               112 784 136 B
ConcurrentQueue<TestObjectRef>:                             64 783 536 B
ConcurrentStack<TestObject>:                               128 005 072 B
ConcurrentStack<TestObjectRef>:                             80 004 632 B

对于内存测试,我发现最好用的

GC.GetAllocatedBytesForCurrentThread()

1

对于结构/值的数组,我得到以下不同的结果:

first = Marshal.UnsafeAddrOfPinnedArrayElement(array, 0).ToInt64();
second = Marshal.UnsafeAddrOfPinnedArrayElement(array, 1).ToInt64();
arrayElementSize = second - first;

(简化的例子)

无论采用哪种方法,您实际上都需要了解.Net如何正确解释结果。例如,返回的元素大小是“对齐的”元素大小,并带有一些填充。开销以及大小的不同取决于类型的用法:在GC堆上,在堆栈上,在字段上,在数组元素上都是“装箱”。

(我想知道使用“虚拟”空结构(没有任何字段)来模仿泛型的“可选”参数会对内存产生什么影响;使用涉及空结构的不同布局进行测试,我可以看到一个空结构会使用(至少)每个元素1个字节;我隐约记得这是因为.Net需要为每个字段使用不同的地址,如果字段确实为空/ 0大小,则该地址将无效。


0

最简单的方法是: int size = *((int*)type.TypeHandle.Value + 1)

我知道这是实现细节,但GC依赖它,因此它必须尽可能接近方法表的开头,以提高效率,还要考虑到GC代码复杂性是如何在将来没人敢更改的。实际上,它适用于.net framework + .net核心的每个次要/主要版本。(当前无法测试1.0)
如果想要更可靠的方法,请在动态程序集中[StructLayout(LayoutKind.Auto)]以完全相同的字段以相同顺序发出结构,并使用sizeof IL指令获取其大小。您可能想要在struct中发出一个静态方法,该方法仅返回此值。然后为对象标题添加2 * IntPtr.Size。这应该给您确切的价值。
但是,如果您的类是从另一个类派生的,则需要分别查找基类的每个大小,并将它们再加上+ 2 * Inptr.Size作为标头。您可以通过获取带有字段来实现此目的,BindingFlags.DeclaredOnly标志的。
数组和字符串只是将其长度*元素大小相加。对于累积对象的累积大小,您需要实施更复杂的解决方案,其中涉及访问每个字段并检查其内容。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.