使用C#获取字段的字节大小

64

我有一个类,想要检查它的字段并报告每个字段占用了多少字节。假设所有字段的类型都是Int32、byte等。

如何轻松地找出每个字段占用了多少字节?

我需要像这样的信息:

Int32 a;
// int a_size = a.GetSizeInBytes;
// a_size should be 4
9个回答

106
基本上是不可能的。它将取决于填充,这可能基于您使用的CLR版本和处理器等。更容易计算对象的总大小,假设它没有引用其他对象:创建一个大数组,使用GC.GetTotalMemory作为基础点,将数组填充到新实例的引用中,然后再次调用GetTotalMemory。从另一个值中减去一个值,然后除以实例数。您应该先创建一个单个实例,以确保没有新的JIT代码对数字产生贡献。是的,它就像听起来那样笨拙 - 但我以前已经成功使用过它。
就在昨天,我还在考虑写一个小助手类来完成这个任务。如果您有兴趣,请告诉我。
编辑:有两个其他建议,我想同时解决它们。
首先是sizeof运算符:这只显示类型在抽象中占用了多少空间,没有填充应用在其周围。(它包括结构内的填充,但不包括在另一种类型的变量中应用的填充。)
接下来是Marshal.SizeOf:这只显示封送后的非托管大小,而不是内存中的实际大小。正如文档明确指出的那样:
返回结果:

返回的大小实际上是未托管类型的大小。对象的托管和未托管大小可能不同。对于字符类型,大小受应用于该类的CharSet值的影响。

再次强调,填充可能会产生差异。

为了澄清我所说的填充有关,考虑这两个类:

class FourBytes { byte a, b, c, d; }
class FiveBytes { byte a, b, c, d, e; }

在我的x86计算机上,FourBytes的实例占用12个字节(包括开销)。FiveBytes的实例需要16个字节。唯一的区别在于“e”变量-那么它需要4个字节吗?嗯,有点...但也不完全是。显然,你可以从FiveBytes中删除任何单个变量以将大小降回到12个字节,但这并不意味着每个变量都占用4个字节(考虑删除所有变量!)。单个变量的成本在这里并没有很多意义。

6
@jon - 我对你的辅助类很感兴趣... 有没有提供? - gap
@gap:不好意思,我从未编写过这个辅助类。 - Jon Skeet
这个答案有任何新的进展吗?肯定有比依赖垃圾回收器更好的方法。另外,ldelema 怎么样?如果你深入到 IL 级别,那么 ldelema 是否可靠地确定给定对象的大小呢?(如果你取出 T 数组中相邻两个元素的地址,然后得到它们之间的差异)...或者说,sizeof 指令应该正好做到这个问题所要求的。 - Earlz
@JonSkeet 我的意思是,作为C#结构的sizeof与IL中的sizeof指令完全不同。 - Earlz
@JonSkeet 看看我的答案。如果你降到IL级别,完全可以获取这些信息。 - Earlz
显示剩余2条评论

17

根据提问者的需求,Marshal.SizeOf 可能会或可能不会给你想要的结果。(在 Jon Skeet 发布他的答案后进行了编辑)。

using System;
using System.Runtime.InteropServices;

public class MyClass
{
    public static void Main()
    {
        Int32 a = 10;
        Console.WriteLine(Marshal.SizeOf(a));
        Console.ReadLine();
    }
}

请注意,正如jkersch所说,可以使用sizeof,但可惜的是只能用于值类型。如果您需要类的大小,则应使用Marshal.SizeOf。

Jon Skeet已经说明了为什么sizeof和Marshal.SizeOf都不完美。我猜测提问者需要决定是否接受其中任何一个来解决他的问题。


问题说:“我想检查它的字段,并最终报告每个字段占用多少字节。我假设所有字段都是Int32、byte等类型。”,所以我认为Jon Skeet没有考虑类开销。 - Windows programmer
不是类的开销的问题 - 而是在编组之后,对象在内存中的样子可能会与它看起来的样子非常不同。对我来说,编组就像黑魔法一样...但编组后的大小很可能与内存中的大小不同。我会编辑我的答案。 - Jon Skeet
我猜只有原帖作者才能确定哪个是可接受的,哪个不是。我修改了我的回答并进行了维基化。 - Lasse V. Karlsen

12

根据Jon Skeet在他的回答中提到的方法,我尝试制作了助手类。欢迎提出改进建议。

public class MeasureSize<T>
{
    private readonly Func<T> _generator;
    private const int NumberOfInstances = 10000;
    private readonly T[] _memArray;

    public MeasureSize(Func<T> generator)
    {
        _generator = generator;
        _memArray = new T[NumberOfInstances];
    }

    public long GetByteSize()
    {
        //Make one to make sure it is jitted
        _generator();

        long oldSize = GC.GetTotalMemory(false);
        for(int i=0; i < NumberOfInstances; i++)
        {
            _memArray[i] = _generator();
        }
        long newSize = GC.GetTotalMemory(false);
        return (newSize - oldSize) / NumberOfInstances;
    }
}

使用方式:

应该使用生成新的T实例的Func来创建。确保不要每次返回相同的实例。例如,以下方法是正确的:

    public long SizeOfSomeObject()
    {
        var measure = new MeasureSize<SomeObject>(() => new SomeObject());
        return measure.GetByteSize();
    }

我会将“generator”更改为更标准的术语“factory”。 - redcalx
这种方法没有考虑到对_memArray的初始内存分配。也就是说,在_memArray = new T[NumberOfInstances];之前测量oldSize是有意义的。 - Alexey Khoroshikh
4
@Alexey,但你不想测量_memArray!它只是一个帮助数组,用于包含您想要测量的内容,即T的大小。这就是这个类的全部意义。 - Jesper Niedermann
是的,@Jesper,从理论上讲,您绝对是正确的,这意味着“纯”类实例大小。然而,如果谈论现实世界的例子(例如,“可以加载多少实例到空闲内存中”,我遇到的问题),就必须考虑到对此实例的引用。因此,尽管它在理论上不正确,但我的估计更有用。显然,我必须在先前的评论中包含这一点。对我带来的误解很抱歉。 - Alexey Khoroshikh
1
@Alexey 好的,我明白你的观点。但是我猜数组大小与数组内容相比应该是微不足道的。不是吗?此外,最初的问题更多地是关于测量单个对象的大小。但当然,随意根据需要更改代码 :) - Jesper Niedermann
显示剩余3条评论

5
我必须将这个功能压缩到IL级别,但最终我使用了一个非常小的库在C#中实现了这个功能。
您可以在 bitbucket 上获取它(BSD许可)。
示例代码:
using Earlz.BareMetal;

...
Console.WriteLine(BareMetal.SizeOf<int>()); //returns 4 everywhere I've tested
Console.WriteLine(BareMetal.SizeOf<string>()); //returns 8 on 64-bit platforms and 4 on 32-bit
Console.WriteLine(BareMetal.SizeOf<Foo>()); //returns 16 in some places, 24 in others. Varies by platform and framework version

...

struct Foo
{
  int a, b;
  byte c;
  object foo;
}

基本上,我所做的是在sizeof IL指令周围编写了一个快速的类方法包装器。该指令将获取引用对象使用的原始内存量。例如,如果您有一个T数组,则sizeof指令将告诉您每个数组元素之间相隔多少字节。
这与C#的sizeof运算符非常不同。首先,C#只允许纯值类型,因为静态方式无法获取其他任何内容的大小。相比之下,sizeof指令在运行时级别工作。因此,在此特定实例期间使用对类型的引用将返回使用的内存量。
您可以在我的博客上查看更多信息和一些更深入的示例代码。

3
@JonSkeet 当然,这也适用于结构体。基本上没有可能得到“每个上下文中byte会是什么”,因为在不同的上下文中它不同。但是,这肯定会有所帮助。(如果其他方法都失败了,可以逐个字段地向结构添加字段,并通过此方法测量每个字段的大小) - Earlz
1
@JonSkeet 嗯,我创建这个的原因是为了确定包含结构体的数组在原始字节中的大小,以避免大对象堆。除此之外,你是正确的,我看不出还有什么好处,除非你正在进行一些奇怪的指针算术或其他在C#中被反对的东西。 - Earlz
1
很遗憾,否则是不可能的 :( 反射 API 不会公开结构体 sizeof 操作码。但是,如果你想的话,可以在后期构建步骤中使用诸如 ILMerge 的工具来避免分发单独的程序集。 - Earlz
1
@Qwertie 使用DynamicMethod实现简单的IL非常容易;您不需要一个完整的独立库/ dll。例如,请查看此答案,它还提供了另一种方法来回答ValueType - 或者更有趣的是,“Formatted Class” - in-situ大小问题。在那个答案中,我提供了一个通用的托管指针减法函数,在IL中。它返回一个字节偏移量,因此支持与不同类型一起使用,因此它也可以测量任何结构体的大小,只要给定的东西被知道是pack=1相邻的。 - Glenn Slayden
@Earlz 回复:“*...创建这个的原因是为了确定包含结构体的数组的大小...作为避免[LOH]的一种方式...*”天啊,我也是!现在我想知道,鉴于 .NET 已经彻底而完美地封锁了“运行时结构布局”的这个领域,我们两个恰好碰到的这个精确用例是否是他们没有预见到的唯一一个。 - Glenn Slayden
显示剩余6条评论

5
它可以间接完成,而不考虑对齐方式。引用类型实例的字节数等于服务字段大小加上类型字段大小。服务字段(32位占4个字节,64位占8个字节):
  1. Sysblockindex
  2. 指向方法表的指针
  3. +可选项(只适用于数组) 数组大小
因此,对于没有任何字段的类,它在32位机器上占用8个字节的空间。如果这是具有一个字段的类,则对同一类实例的引用也需要考虑在内 (64位):

Sysblockindex + pMthdTable + 类的引用 = 8 + 8 + 8 = 24个字节

如果它是值类型,则不具有任何实例字段,因此仅占据其字段大小。例如,如果我们有一个具有一个整型字段的结构体,则在32位机器上只需占用4个字节的内存。

1
如果您有类型,请使用sizeof运算符。它将返回类型的字节大小。例如:

Console.WriteLine(sizeof(int));

将输出:

4


尽管您可以使用SizeOf方法,但此方法返回的值并不总是与sizeof返回的值相同。Marshal.SizeOf在类型进行封送后返回大小,而sizeof返回由CLR分配的大小,包括任何填充。 - Philip Fourie
MSDN链接:ms-help://MS.VSCC.v90/MS.MSDNQTR.v90.en/dv_csref/html/c548592c-677c-4f40-a4ce-e613f7529141.htm - Philip Fourie

0
您可以使用方法重载作为一个技巧来确定字段大小:
public static int FieldSize(int Field) { return sizeof(int); }
public static int FieldSize(bool Field) { return sizeof(bool); }
public static int FieldSize(SomeStructType Field) { return sizeof(SomeStructType); }

0

System.Runtime.CompilerServices.Unsafe

使用 System.Runtime.CompilerServices.Unsafe.SizeOf<T>() where T: unmanaged

(当不在 .NET Core 中运行时,您需要安装该 NuGet 包)

文档 表示:

返回给定类型参数的对象大小。

它似乎像 Earlz 解决方案 一样使用了 sizeof IL 指令。(来源)

unmanaged 约束是 C# 7.3 中的新功能。


0

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

我知道这是实现细节,但GC依赖它,并且为了效率,它需要尽可能靠近方法表的开头,考虑到GC代码的复杂性,未来没有人敢改变它。事实上,它适用于每个 .net framework+.net core 的次要/主要版本。(目前无法测试1.0)
如果您想要更可靠的方法,请使用[StructLayout(LayoutKind.Auto)]在动态程序集中发出一个结构体,具有相同顺序的完全相同字段,使用sizeof IL指令获取其大小。您可能需要在结构体中发出一个静态方法,该方法仅返回此值。然后添加2 * IntPtr.Size以获取对象标头。这应该给您确切的值。
但是,如果您的类派生自另一个类,则需要分别找到每个基类的大小,并再次添加它们+ 2 * Inptr.Size以获取标头。您可以使用BindingFlags.DeclaredOnly标志获取字段来执行此操作。


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