【发布时间】:2010-11-10 19:53:34
【问题描述】:
对于任意实例(不同对象的集合、组合、单个对象等)
如何确定其大小(以字节为单位)?
(我目前有各种对象的集合,我正在尝试确定它的聚合大小)
编辑:有人为 Object 编写了一个扩展方法可以做到这一点吗?这将是相当整洁的 imo。
【问题讨论】:
标签: c#
对于任意实例(不同对象的集合、组合、单个对象等)
如何确定其大小(以字节为单位)?
(我目前有各种对象的集合,我正在尝试确定它的聚合大小)
编辑:有人为 Object 编写了一个扩展方法可以做到这一点吗?这将是相当整洁的 imo。
【问题讨论】:
标签: c#
【讨论】:
AFAIK,如果不深入计算每个成员的大小(以字节为单位),您就无法做到。但同样,成员的大小(如集合中的元素)是否计入对象的大小,或者指向该成员的指针是否计入对象的大小?取决于你如何定义它。
之前我曾遇到过这种情况,我想根据对象消耗的内存来限制缓存中的对象。
好吧,如果有什么技巧可以做到这一点,我很高兴知道!
【讨论】:
您可以使用反射来收集所有公共成员或属性信息(给定对象的类型)。但是,如果不遍历对象上的每条数据,就无法确定大小。
【讨论】:
这不适用于当前的 .NET 实现,但是对于垃圾收集/托管运行时要记住的一件事是,对象的分配大小可以在程序的整个生命周期内发生变化。例如,一些分代垃圾收集器(如Generational/Ulterior Reference Counting Hybrid collector)只需要在对象从nursery 移动到mature 空间后存储某些信息。
这使得创建可靠的通用 API 来公开对象大小变得不可能。
【讨论】:
如果您正在使用可序列化的对象,您可以通过假装使用二进制序列化程序对其进行序列化(但将输出路由到遗忘)来近似大小。
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();
}
}
【讨论】:
对于值类型,您可以使用Marshal.SizeOf。当然,它返回在非托管内存中编组结构所需的字节数,这不一定是 CLR 使用的。
【讨论】:
首先,警告:以下内容仅限于丑陋、无证的黑客行为。不要依赖这个工作 - 即使它现在对你有用,它可能会在明天停止工作,任何次要或主要的 .NET 更新。
您可以使用这篇文章中关于 CLR 内部MSDN Magazine Issue 2005 May - Drill Into .NET Framework Internals to See How the CLR Creates Runtime Objects 的信息 - 我上次检查过,它仍然适用。这是如何完成的(它通过类型的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。对于他们,您需要将所有包含元素的大小添加到它们的基本实例大小中。
【讨论】:
Cannot take the address of, get the size of, or declare a pointer to a managed type ('System.RuntimeTypeHandle')
Marshal.ReadInt32(type.TypeHandle.Value, 4) 适用于 x86 和 x64。我只测试了结构和类类型。请记住,这会返回值类型的 boxed 大小。 @Pavel 也许你可以更新你的答案。
type 替换为obj.GetType()。使用哪个框架并不重要,只需要 CLR(v2 或 v4 或 CoreCLR)。我还没有在 CoreCLR 上尝试过。
使用Son Of Strike,它有一个命令ObjSize。
请注意,实际消耗的内存始终大于ObjSize 报告,因为synkblk 直接位于对象数据之前。
【讨论】:
对于非托管类型,也就是值类型,结构:
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),您将获得以字节为单位的内容,这不计算类或引用的权重,但更接近现实。 如果大小很重要,我建议使用字节数组存储数据,使用非托管代理类访问使用指针转换的值,请注意,这将是非对齐内存,因此在旧计算机上会很慢,但现代 RAM 上的巨大数据集将是快得多,因为最小化从 RAM 读取的大小将比未对齐产生更大的影响。
【讨论】:
安全解决方案,进行了一些优化 CyberSaving/MemoryUsage code。 一些案例:
/* 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
【讨论】:
对于寻求不需要[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;
}
}
不,这不会为您提供将在内存中使用的确切大小。如前所述,这是不可能的。但它会给你一个粗略的估计。
请注意,这也很慢。
【讨论】:
来自 Pavel 和 jnm2:
private int DumpApproximateObjectSize(object toWeight)
{
return Marshal.ReadInt32(toWeight.GetType().TypeHandle.Value, 4);
}
请注意,它只适用于连续的内存对象
【讨论】:
最简单的方法是:int size = *((int*)type.TypeHandle.Value + 1)
我知道这是实现细节,但 GC 依赖于它,它需要尽可能接近方法表的开始以提高效率,并考虑到 GC 代码的复杂程度,以后没人敢更改它。事实上,它适用于 .net framework+.net core 的每个次要/主要版本。 (目前无法测试 1.0)
如果您想要更可靠的方法,请在具有 [StructLayout(LayoutKind.Auto)] 的动态程序集中发出一个结构,并以相同的顺序使用完全相同的字段,并使用 sizeof IL 指令获取其大小。您可能希望在 struct 中发出一个静态方法,该方法仅返回此值。然后为对象头添加 2*IntPtr.Size。这应该会给你准确的价值。
但是,如果您的类派生自另一个类,则需要分别找到基类的每个大小并再次添加它们 + 2*Inptr.Size 作为标题。您可以通过获取带有BindingFlags.DeclaredOnly 标志的字段来做到这一点。
数组和字符串只是将该大小添加到它的长度 * 元素大小。
对于聚合对象的累积大小,您需要实施更复杂的解决方案,包括访问每个字段并检查其内容。
【讨论】:
我已经为 .NET 中的不同集合创建了基准测试:https://github.com/scholtz/TestDotNetCollectionsMemoryAllocation
.NET Core 2.2 分配了 1,000,000 个对象并分配了 3 个属性的结果如下:
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()
【讨论】:
对于结构/值的数组,我有不同的结果:
first = Marshal.UnsafeAddrOfPinnedArrayElement(array, 0).ToInt64();
second = Marshal.UnsafeAddrOfPinnedArrayElement(array, 1).ToInt64();
arrayElementSize = second - first;
(过于简单的例子)
无论采用何种方法,您确实需要了解 .Net 如何正确解释结果。 例如,返回的元素大小是“对齐的”元素大小,带有一些填充。 开销和大小因使用类型而异:在 GC 堆上、堆栈上、作为字段、作为数组元素“装箱”。
(我想知道使用“虚拟”空结构(没有任何字段)来模拟泛型的“可选”参数对内存的影响;使用涉及空结构的不同布局进行测试,我可以看到空struct 每个元素(至少)使用 1 个字节;我隐约记得这是因为 .Net 需要为每个字段使用不同的地址,如果字段确实为空/0 大小,则该地址将不起作用)。
【讨论】: