如何计算一个实例占用多少内存?

这篇具有很好参考价值的文章主要介绍了如何计算一个实例占用多少内存?。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

我们都知道CPU和内存是程序最为重要的两类指标,那么有多少人真正想过这个问题:一个类型(值类型或者引用类型)的实例在内存中究竟占多少字节?我们很多人都回答不上来。其实C#提供了一些用于计算大小的操作符和API,但是它们都不能完全解决我刚才提出的问题。本文提供了一种计算值类型和引用类型实例所占内存字节数量的方法。源代码从这里下载。

一、sizeof操作符
二、Marshal.SizeOf方法
三、Unsafe.SizeOf方法>
四、可以根据字段成员的类型来计算吗?
五、值类型和应用类型的布局
六、Ldflda指令
七、计算值类型的字节数
八、计算引用类型字节数
九、完整的计算

一、sizeof操作符

sizeof操作用来确定某个类型对应实例所占用的字节数,但是它只能应用在Unmanaged类型上。所谓的Unmanaged类型仅限于:

  • 原生类型(Primitive Type:Boolean, Byte, SByte, Int16, UInt16, Int32, UInt32, Int64, UInt64, IntPtr, UIntPtr, Char, Double, 和Single)
  • Decimal类型
  • 枚举类型
  • 指针类型
  • 只包含Unmanaged类型数据成员的结构体

顾名思义,一个Unmanaged类型是一个值类型,对应的实例不能包含任何一个针对托管对象的引用。如果我们定义如下这样一个泛型方法来调用sizeof操作符,泛型参数T必须添加unmananged约束,而且方法上还得添加unsafe标记。

public static unsafe int SizeOf<T>() where T : unmanaged => sizeof(T);

只有原生类型和枚举类型可以直接使用sizeof操作符,如果将它应用在其他类型(指针和自定义结构体),必须添加/unsafe编译标记,还需要放在unsafe上下文中。

Debug.Assert(sizeof(byte) == 1);
Debug.Assert(sizeof(sbyte) == 1);
Debug.Assert(sizeof(short) == 2);
Debug.Assert(sizeof(ushort) == 2);
Debug.Assert(sizeof(int) == 4);
Debug.Assert(sizeof(uint) == 4);
Debug.Assert(sizeof(long) == 8);
Debug.Assert(sizeof(ulong) == 8);
Debug.Assert(sizeof(char) == 2);
Debug.Assert(sizeof(float) == 4);
Debug.Assert(sizeof(double) == 8);
Debug.Assert(sizeof(bool) == 1);
Debug.Assert(sizeof(decimal) == 16);
Debug.Assert(sizeof(DateTimeKind) == 4);

unsafe

{

    Debug.Assert(sizeof(int*) == 8);
    Debug.Assert(sizeof(DateTime) == 8);
    Debug.Assert(sizeof(DateTimeOffset) == 16);
    Debug.Assert(sizeof(Guid) == 16);
    Debug.Assert(sizeof(Point) == 8);

}

由于如下这个结构体Foobar并不是一个Unmanaged类型,所以程序会出现编译错误。

unsafe
{
    Debug.Assert(sizeof(Foobar) == 16);
}
public struct Foobar
{
    public string Foo;
    public int Bar;
}

二、Marshal.SizeOf方法

静态类型Marshal定义了一系列API用来帮助我们完成非托管内存的分配与拷贝、托管类型和非托管类型之间的转换,以及其他一系列非托管内存的操作(Marshal在计算科学中表示为了数据存储或者传输而将内存对象转换成相应的格式的操作)。静态其中就包括如下4个SizeOf方法重载来确定指定类型或者对象的字节数。

public static class Marshal
{
    public static int SizeOf(object structure);
    public static int SizeOf<T>(T structure);
    public static int SizeOf(Type t);
    public static int SizeOf<T>()
}
Marshal.SizeOf方法虽然对指定的类型没有针对Unmanaged类型的限制,但是依然要求指定一个值类型。如果传入的是一个对象,该对象也必须是对一个值类型的装箱。
object  value = default(Foobar);
Debug.Assert(Marshal.SizeOf<Foobar>() == 16);
Debug.Assert(Marshal.SizeOf(value) == 16);
Debug.Assert(Marshal.SizeOf(typeof(Foobar)) == 16);
Debug.Assert(Marshal.SizeOf(typeof(Foobar)) == 16);

public struct Foobar
{
    public object Foo;
    public object Bar;
}

由于如下这个Foobar被定义成了,所以针对两个SizeOf方法的调用都会抛出ArgumentException异常,并提示:Type 'Foobar' cannot be marshaled as an unmanaged structure; no meaningful size or offset can be computed.

Marshal.SizeOf<Foobar>();
Marshal.SizeOf(new Foobar());

public class Foobar
{
    public object Foo;
    public object Bar;
}

Marshal.SizeOf方法不支持泛型,还对结构体的布局有要求,它支持支SequentialExplicit布局模式。由于如下所示的Foobar结构体采用Auto布局模式(由于非托管环境具有更加严格的内存布局要求,所以不支持Auto这种根据字段成员对内存布局进行“动态规划”的方式),所以针对SizeOf方法的调用还是会抛出和上面一样的ArgumentException异常。

Marshal.SizeOf<Foobar>();

[StructLayout(LayoutKind.Auto)]
public struct Foobar
{
    public int Foo;
    public int Bar;
}

三、Unsafe.SizeOf方法>

静态Unsafe提供了针对非托管内存更加底层的操作,类似的SizeIOf方法同样定义在该类型中。该方法对指定的类型没有任何限制,但是如果你指定的是引用类型,它会返回“指针字节数”(IntPtr.Size)。

public static class Unsafe
{
    public static int SizeOf<T>();
}
Debug.Assert( Unsafe.SizeOf<FoobarStructure>() == 16);
Debug.Assert( Unsafe.SizeOf<FoobarClass>() == 8);

public struct FoobarStructure
{
    public long Foo;
    public long Bar;
}

public class FoobarClass
{
    public long Foo;
    public long Bar;
}

四、可以根据字段成员的类型来计算吗?

我们知道不论是值类型还是引用类型,对应的实例都映射为一段连续的片段(或者直接存储在寄存器)。类型的目的就在于规定了对象的内存布局,具有相同类型的实例具有相同的布局,字节数量自然相同(对于引用类型的字段,它在这段字节序列中只存储引用的地址)。既然字节长度由类型来决定,如果我们能够确定每个字段成员的类型,那么我们不就能够将该类型对应的字节数计算出来吗?实际上是不行的。

Debug.Assert(Unsafe.SizeOf<ValueTuple<byte, byte>>() == 2);
Debug.Assert(Unsafe.SizeOf<ValueTuple<byte, short>>() == 4);
Debug.Assert(Unsafe.SizeOf<ValueTuple<byte, int>>() == 8);
Debug.Assert(Unsafe.SizeOf<ValueTuple<byte, long>>() == 16);

一上面的程序为例,我们知道byte、short、int和long的字节数分别是1、2、4和8,所以一个针对byte的二元组的字节数为2,但是对于一个针对类型组合分别为byte + short,byte + int,byte + long的二元组来说,对应的字节并不是3、5和9,而是3、8和16。因为这涉及内存对齐(memory alignment)的问题。

五、值类型和引用类型的布局

对于完全相同的数据成员,引用类型和子类型的实例所占的字节数也是不同的。如下图所示,值类型实例的字节序列全部用来存储它的字段成员。对于引用类型的实例来说,在字段字节序列前面还存储了类型对应方法表(Method Table)的地址。方法表几乎提供了描述类型的所有元数据,我们正是利用这个引用来确定实例属于何种类型。在最前面,还具有额外的字节,我们将其称为Object Header,它不仅仅用来存储对象的锁定状态,哈希值也可以缓存在这里。当我们创建了一个引用类型变量时,这个变量并不是指向实例所占内存的首字节,而是存放方法表地址的地方

如何计算一个实例占用多少内存?

六、Ldflda指令

上面我们介绍sizeof操作符和静态类型Marshal/Unsafe提供的SizeOf方法均不能真正解决实例占用字节长度的计算。就我目前的了解,这个问题在单纯的C#领域都无法解决,但IL层面提供的Ldflda指令可以帮助我们解决这个问题。顾名思义,Ldflda表示Load Field Address,它可以帮助我们得到实例某个字段的地址。由于这个IL指令在C#中没有对应的API,所以我们只有采用如下的形式采用IL Emit的来使用它。

public class SizeCalculator
{
    private static Func<object?, long[]> GenerateFieldAddressAccessor(FieldInfo[] fields)
    {
        var method = new DynamicMethod(
            name: "GetFieldAddresses",
            returnType: typeof(long[]),
            parameterTypes: new[] { typeof(object) },
            m: typeof(SizeCalculator).Module,
            skipVisibility: true);
        var ilGen = method.GetILGenerator();

        // var addresses = new long[fields.Length + 1];
        ilGen.DeclareLocal(typeof(long[]));
        ilGen.Emit(OpCodes.Ldc_I4, fields.Length + 1);
        ilGen.Emit(OpCodes.Newarr, typeof(long));
        ilGen.Emit(OpCodes.Stloc_0);

        // addresses[0] = address of instace;
        ilGen.Emit(OpCodes.Ldloc_0);
        ilGen.Emit(OpCodes.Ldc_I4, 0);
        ilGen.Emit(OpCodes.Ldarg_0);
        ilGen.Emit(OpCodes.Conv_I8);
        ilGen.Emit(OpCodes.Stelem_I8);

        // addresses[index] = address of field[index + 1];
        for (int index = 0; index < fields.Length; index++)
        {
            ilGen.Emit(OpCodes.Ldloc_0);
            ilGen.Emit(OpCodes.Ldc_I4, index + 1);
            ilGen.Emit(OpCodes.Ldarg_0);
            ilGen.Emit(OpCodes.Ldflda, fields[index]);
            ilGen.Emit(OpCodes.Conv_I8);
            ilGen.Emit(OpCodes.Stelem_I8);
        }

        ilGen.Emit(OpCodes.Ldloc_0);
        ilGen.Emit(OpCodes.Ret);

        return (Func<object?, long[]>)method.CreateDelegate(typeof(Func<object, long[]>));
    }
    ...
}

如上面的代码片段所示,我们在SizeCalculator类型中定了一个GenerateFieldAddressAccessor方法,它会根据指定类型的字段列表生成一个Func<object?, long[]> 类型的委托,该委托帮助我们返回指定对象及其所有字段的内存地址。有了对象自身的地址和每个字段的地址,我们自然就可以得到每个字段的偏移量,进而很容易地计算出整个实例所占内存的字节数。

七、计算值类型的字节数

由于值类型和引用类型在内存中采用不同的布局,我们也需要采用不同的计算方式。由于结构体在内存中字节就是所有字段的内容,所有我们采用一种讨巧的计算方法。假设我们需要结算类型为T的结构体的字节数,那么我们创建一个ValueTuple<T,T>元组,它的第二个字段Item2的偏移量就是结构体T的字节数。具体的计算方式体现在如下这个CalculateValueTypeInstance方法中。

public class SizeCalculator
{
    private static readonly MethodInfo _getDefaultMethod = typeof(SizeCalculator).GetMethod(nameof(GetDefault), BindingFlags.Static | BindingFlags.NonPublic)!;

    private int CalculateValueTypeInstance(Type type)
    {
        var instance = GetDefaultAsObject(type);
        var fields = type.GetFields(BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
            .Where(it => !it.IsStatic)
            .ToArray();

        if (fields.Length == 0) return 0;
        var tupleType = typeof(ValueTuple<,>).MakeGenericType(type, type);
        var tupple = tupleType.GetConstructors()[0].Invoke(new object?[] { instance, instance });
        var addresses = GenerateFieldAddressAccessor(tupleType.GetFields()).Invoke(tupple).OrderBy(it => it).ToArray();
        return (int)(addresses[2] - addresses[0]);
    }

    private static T GetDefault<T>() where T : struct => default!;
    private static object? GetDefaultAsObject(Type type) => _getDefaultMethod.MakeGenericMethod(type).Invoke(null, Array.Empty<object>());
}

如上面的代码片段所示, 假设我们需要计算的结构体类型为T,我们调用GetDefaultAsObject方法以反射的形式得到default(T)对象,进而将ValueTuple<T,T>元组创建出来。在调用GenerateFieldAddressAccessor方法得到用于计算实例及其字段地址的Func<object?, long[]> 委托后,我们将这个元组作为参数调用这个委托。对于得到的三个内存地址,代码元组和第1、2个字段的地址是相同的,我们使用代表Item2的第三个地址减去第一个地址,得到的就是我们希望的结果。

八、计算引用类型字节数

引用类型的字节计算要复杂一些,具体采用这样的思路:我们在得到实例自身和每个字段的地址后,我们对地址进行排序进而得到最后一个字段的偏移量。我们让这个偏移量加上最后一个字段自身的字节数,再补充上必要的“头尾字节”就是我们希望得到的结果,具体计算体现在如下这个CalculateReferneceTypeInstance方法上。

public class SizeCalculator
{
    private int CalculateReferenceTypeInstance(Type type, object instance)
    {
        var fields = GetBaseTypesAndThis(type)
            .SelectMany(type => type.GetFields(BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
            .Where(it => !it.IsStatic).ToArray();

        if (fields.Length == 0) return type.IsValueType ? 0 : 3 * IntPtr.Size;
        var addresses = GenerateFieldAddressAccessor(fields).Invoke(instance);
        var list = new List<FieldInfo>(fields);
        list.Insert(0, null!);
        fields = list.ToArray();
        Array.Sort(addresses, fields);

        var lastFieldOffset = (int)(addresses.Last() - addresses.First());
        var lastField = fields.Last();
        var lastFieldSize = lastField.FieldType.IsValueType ? CalculateValueTypeInstance(lastField.FieldType) : IntPtr.Size;
        var size = lastFieldOffset + lastFieldSize;

        // Round up to IntPtr.Size
        int round = IntPtr.Size - 1;
        return ((size + round) & (~round)) + IntPtr.Size;

        static IEnumerable<Type> GetBaseTypesAndThis(Type? type)
        {
            while (type is not null)
            {
                yield return type;
                type = type.BaseType;
            }
        }
    }
}

如上面的代码片段所示,如果指定的类型没有定义任何字段,CalculateReferneceTypeInstance 返回引用类型实例的最小字节数:3倍地址指针字节数。对于x86架构,一个应用类型对象至少占用12字节,包括ObjectHeader(4 bytes)、方法表指针(bytes)和最少4字节的字段内容(即使没有类型没有定义任何字段,这个4个字节也是必需的)。如果是x64架构,这个最小字节数会变成24,因为方法表指针和最小字段内容变成了8个字节,虽然ObjectHeader的有效内容只占用4个字节,但是前面会添加4个字节的Padding。

对于最后字段所占字节的结算也很简单:如果类型是值类型,那么就调用前面定义的CalculateValueTypeInstance方法进行计算,如果是引用类型,字段存储的内容仅仅是目标对象的内存地址,所以长度就是IntPtr.Size。由于引用类型实例在内存中默认会采用IntPtr.Size对齐,这里也做了相应的处理。最后不要忘了,引用类型实例的引用指向的并不是内存的第一个字节,而是存放方法表指针的字节,所以还得加上ObjecthHeader 字节数(IntPtr.Size)。

九、完整的计算

分别用来计算值类型和引用类型实例字节数的两个方法被用在如下这个SizeOf方法中。由于Ldflda指令的调用需要提供对应的实例,所以该方法除了提供目标类型外,还提供了一个用来获得对应实例的委托。该委托对应的参数是可以缺省的,对于值类型,我们会使用默认值。对于引用类型,我们也会试着使用默认构造函数来创建目标对象。如果没有提供此委托对象,也无法创建目标实例,SizeOf方法会抛出异常。虽然需要提供目标实例,但是计算出的结果只和类型有关,所以我们将计算结果进行了缓存。为了调用方便,我们还提供了另一个泛型的SizeOf<T>方法。

public class SizeCalculator
{
    private static readonly ConcurrentDictionary<Type, int> _sizes = new();
    public static readonly SizeCalculator Instance = new();
    public int SizeOf(Type type, Func<object?>? instanceAccessor = null)
    {
        if (_sizes.TryGetValue(type, out var size)) return size;
        if (type.IsValueType) return _sizes.GetOrAdd(type, CalculateValueTypeInstance);

        object? instance;
        try
        {
            instance = instanceAccessor?.Invoke() ?? Activator.CreateInstance(type);
        }
        catch
        {
            throw new InvalidOperationException("The delegate to get instance must be specified.");
        }

        return _sizes.GetOrAdd(type, type => CalculateReferenceTypeInstance(type, instance));
    }
    public int SizeOf<T>(Func<T>? instanceAccessor = null)
    {
        if (instanceAccessor is null) return SizeOf(typeof(T));
        Func<object?> accessor = () => instanceAccessor();
        return SizeOf(typeof(T), accessor);
    }
}

在如下的代码片段中,我们使用它输出了两个具有相同字段定义的结构体和类型的字节数。在下一篇文章中,我们将进一步根据计算出的字节数得到实例在内存中的完整二进制内容,敬请关注。文章来源地址https://www.toymoban.com/news/detail-471632.html

Debug.Assert( SizeCalculator.Instance.SizeOf<FoobarStructure>() == 16);
Debug.Assert( SizeCalculator.Instance.SizeOf<FoobarClass>() == 32);

public struct FoobarStructure
{
    public byte Foo;
    public long Bar;
}

public class FoobarClass
{
    public byte Foo;
    public long Bar;
}

到了这里,关于如何计算一个实例占用多少内存?的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 怎么计算flink任务需要多少cpu和内存

    Flink任务需要的CPU和内存取决于任务的具体实现和数据规模。以下是一些常见的方法来评估Flink任务需要多少CPU和内存: 数据规模:Flink任务需要的CPU和内存与数据规模成正比。如果数据规模较大,那么任务需要更多的CPU和内存来处理数据。可以通过以下几种方式来估算数据规

    2024年02月09日
    浏览(40)
  • 如何看内存占用情况,vue反复刷新标签页导致面内存一直在涨,系统反应越来越慢,内存占用4个g。

    内存泄漏(Memory Leak): 不再用到的内存,没有及时释放; 内存溢出(Out Of Memory): 应用系统中存在无法回收的内存或使用的内存过多,最终使得程序运行要用到的内存大于能提供的最大内存。 js 写法(闭包、全局变量等)、 dom 事件监听、循环定时器等这些造成的泄漏; 组件

    2024年02月11日
    浏览(32)
  • 《Linux系列》buff/cache占用太多内存,如何释放内存?

      当遇到很多日志文件时,想要清理部分日志,但是一个一个清理太过麻烦。所以希望通过从文件时间上做逻辑判断,实现把某一时间之前的文件删除。 ll查看所有的日志信息 wc -l统计数量 find查找文件命令 -name指定查找文件的名称 -mtime +n, 查找n天前的文件 -exec 执行脚本固

    2024年02月16日
    浏览(39)
  • 我们在操作自动化测如何实现用例设计实例

    在编写用例之间,笔者再次强调几点编写自动化测试用例的原则: 1、一个脚本是一个完整的场景,从用户登陆操作到用户退出系统关闭浏览器。 2、一个脚本脚本只验证一个功能点,不要试图用户登陆系统后把所有的功能都进行验证再退出系统 3、尽量只做功能中正向逻辑的

    2024年02月05日
    浏览(35)
  • 结构体占用内存大小如何确定?-->结构体字节对齐 | C语言

    目录 一、什么是结构体 二、为什么需要结构体 三、结构体的字节对齐 3.1、示例1 3.2、示例2 3.3、示例3  3.4、示例4 3.5、示例5 四、结构体字节对齐总结         结构体是将不同类型的数据按照一定的功能需 求进行整体封装,封装的数据类型与大小均可以由用户指定。 结

    2024年01月17日
    浏览(46)
  • Mac电脑其他文件占用超过一大半的内存如何清理?

    mac的存储空间时不时会提示内存已满,查看内存占用比例最大的居然是「其他文件」,「其他文件」是Mac无法识别的格式文件或应用插件扩展等等...如果你想要给Mac做一次彻底的磁盘空间清理,首当其冲可先对「其他文件」下手,那么我们该如何清理这些无法识别又无用的「

    2024年02月09日
    浏览(69)
  • Linux如何查看当前占用CPU和内存最多的进程

    查看占用 CPU 最高的前10个进程 查看占用内存(MEM)最高的前10个进程 输入 top 命令,然后按下大写M按照内存MEM排序,按下大写P按照CPU排序

    2024年02月17日
    浏览(50)
  • Linux中该如何查看当前CPU、内存、硬盘占用情况,如何判断当前服务器负载情况

    要查看当前 Linux 系统的 CPU、内存、硬盘占用情况,可以使用以下命令: 查看 CPU 占用情况: 该命令会显示当前系统进程的 CPU 占用情况,以及每个进程占用的 CPU 百分比和内存使用情况等信息。 查看内存占用情况: 该命令会显示当前系统的内存总量、已使用的内存量、空闲

    2024年02月11日
    浏览(69)
  • LLM 推理优化探微 (3) :如何有效控制 KV 缓存的内存占用,优化推理速度?

    编者按: 随着 LLM 赋能越来越多需要实时决策和响应的应用场景,以及用户体验不佳、成本过高、资源受限等问题的出现,大模型高效推理已成为一个重要的研究课题。为此,Baihai IDP 推出 Pierre Lienhart 的系列文章,从多个维度全面剖析 Transformer 大语言模型的推理过程,以期

    2024年03月15日
    浏览(73)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包