用struct替代class后,内存占用竟然更大了

本文探讨在C#中将纯数据class改为struct后内存占用不降反升的现象,核心原因在于struct默认采用Sequential布局导致内存对齐不足,而class默认Auto布局由CLR自动优化字段排列以减少填充。通过StructLayout显式指定Explicit或Sequential并配合FieldOffset可改善内存布局,实测显示合理内存对齐后struct可降至最小内存占用。关键涉及StructLayout、LayoutKind、字段重排、内存填充与CLR内存管理机制。

前言

最近遇到这么一件事,有个函数内存分配很高,想优化一下,因为是纯数据类,因此首先把class改成了struct,结果发现内存分配更高了。

代码

类型定义

public class ClassDefault
{
    public int Id;
    public long Name;
    public bool Active;
    public long Age;
    public bool IsMan;
    public long Date;
    public bool IsStudent;
    public long DateMonth;
    public bool IsWoman;
    public long Dateyear;
}
public struct StructDefault
{
    public int Id;
    public long Name;
    public bool Active;
    public long Age;
    public bool IsMan;
    public long Date;
    public bool IsStudent;
    public long DateMonth;
    public bool IsWoman;
    public long Dateyear;
}

模拟业务的测试代码

    [Benchmark(Baseline = true)]
    public List<ClassDefault> Class_Default()
    {
        var list = new List<ClassDefault>(Count);
        for (int i = 0; i < Count; i++)
        {
            list.Add(new ClassDefault { Id = i, Name = i, Active = true });
        }
        return list;
    }
    [Benchmark]
    public List<StructDefault> Struct_Default()
    {
        var list = new List<StructDefault>(Count);
        for (int i = 0; i < Count; i++)
        {
            list.Add(new StructDefault { Id = i, Name = i, Active = true });
        }
        return list;
    }

测试结果
struct内存分配比class更高

MethodMeanErrorStdDevRatioRatioSDGen0Gen1AllocatedAlloc Ratio
Class_Default640.8 ns10.57 ns9.89 ns1.000.001.15590.03057.09 KB1.00
Struct_Default605.7 ns4.60 ns4.08 ns0.950.021.28360.03727.87 KB1.11

怀疑是struct没做内存对齐

内存对齐

用StructLayout做内存对齐

StructLayout接收一个LayoutKind枚举
附一个AI对LayoutKind 的介绍:
LayoutKind 定义了三种布局方式,行为差异很大:

  • Sequential(顺序布局,值为 0):字段严格按照你在代码中的声明顺序在内存中依次排列。这是 struct的默认布局,因为它能保证内存布局的确定性,方便与 C/C++ 等语言的数据结构进行匹配。
  • Explicit (显式布局,值为 2):你拥有对每个字段内存偏移量的绝对控制权。必须配合 [FieldOffset]特性来精确指定每个字段从对象的起始位置偏移多少字节。这主要用于处理 C/C++ 中的union(联合体)或需要精确映射到特定硬件寄存器的场景。
  • Auto (自动布局,值为 3):字段的排列顺序和内存对齐完全由CLR(公共语言运行时)在运行时自动决定,目的是为了达到最佳的内存使用效率(例如通过重排字段来减少填充)。这是 class 的默认布局。

所以原因很明显了,因为class自动做了内存对齐,而struct没有。

内存布局打印

相同字段的情况下,内存对齐后,class一个占用16字节(对象头)+48字节 = 64字节;
struct占80字节。
所以并不是struct一定无脑比class占内存更少
在这里插入图片描述
在这里插入图片描述

using ObjectLayoutInspector;

var sample = new ClassDefault();
TypeLayout.PrintLayout(sample.GetType());
var sample2 = new StructDefault();
TypeLayout.PrintLayout(sample2.GetType());

特殊情况

如果结构体包含引用类型(如 string),CLR 有时仍然会为了优化而重排字段

[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct StructDefaultWithStr
{
    public int Id;
    public string Name;
    public bool Active;
    public long Age;
    public bool IsMan;
    public long Date;
    public bool IsStudent;
    public long DateMonth;
    public bool IsWoman;
    public long Dateyear;
}

在这里插入图片描述

完整测试代码和结果

MethodMeanErrorStdDevRatioRatioSDGen0Gen1AllocatedAlloc Ratio
Class_Default715.2 ns14.03 ns13.12 ns1.000.001.15590.03057.09 KB1.00
Class_Sequential814.2 ns15.59 ns19.14 ns1.130.031.66610.063910.21 KB1.44
Class_Auto690.6 ns12.98 ns12.14 ns0.970.031.15590.03057.09 KB1.00
Struct_Default682.8 ns13.31 ns13.07 ns0.960.031.28360.03727.87 KB1.11
Struct_Sequential677.9 ns9.16 ns8.12 ns0.950.021.28360.03727.87 KB1.11
Struct_Auto462.6 ns8.75 ns8.60 ns0.650.020.77390.01384.74 KB0.67
using System.Collections.Generic;
using System.Runtime.InteropServices;
using BenchmarkDotNet.Attributes;

namespace ConsoleApp.Benchmarks.MemoryLayouts;

public class ClassDefault
{
    public int Id;
    public long Name;
    public bool Active;
    public long Age;
    public bool IsMan;
    public long Date;
    public bool IsStudent;
    public long DateMonth;
    public bool IsWoman;
    public long Dateyear;
}

[StructLayout(LayoutKind.Sequential)]
public class ClassSequential
{
    public int Id;
    public long Name;
    public bool Active;
    public long Age;
    public bool IsMan;
    public long Date;
    public bool IsStudent;
    public long DateMonth;
    public bool IsWoman;
    public long Dateyear;
}

[StructLayout(LayoutKind.Auto)]
public class ClassAuto
{
    public int Id;
    public long Name;
    public bool Active;
    public long Age;
    public bool IsMan;
    public long Date;
    public bool IsStudent;
    public long DateMonth;
    public bool IsWoman;
    public long Dateyear;
}

public struct StructDefault
{
    public int Id;
    public long Name;
    public bool Active;
    public long Age;
    public bool IsMan;
    public long Date;
    public bool IsStudent;
    public long DateMonth;
    public bool IsWoman;
    public long Dateyear;
}

// 如果结构体包含引用类型(如 string),CLR 有时仍然会为了优化而重排字段
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct StructDefaultWithStr
{
    public int Id;
    public string Name;
    public bool Active;
    public long Age;
    public bool IsMan;
    public long Date;
    public bool IsStudent;
    public long DateMonth;
    public bool IsWoman;
    public long Dateyear;
}

[StructLayout(LayoutKind.Sequential)]
public struct StructSequential
{
    public int Id;
    public long Name;
    public bool Active;
    public long Age;
    public bool IsMan;
    public long Date;
    public bool IsStudent;
    public long DateMonth;
    public bool IsWoman;
    public long Dateyear;
}

[StructLayout(LayoutKind.Auto)]
public struct StructAuto
{
    public int Id;
    public long Name;
    public bool Active;
    public long Age;
    public bool IsMan;
    public long Date;
    public bool IsStudent;
    public long DateMonth;
    public bool IsWoman;
    public long Dateyear;
}

[MemoryDiagnoser]
public class LayoutBenchmark
{
    private const int Count = 100;
    private const string TestName = "Test";

    [Benchmark(Baseline = true)]
    public List<ClassDefault> Class_Default()
    {
        var list = new List<ClassDefault>(Count);
        for (var i = 0; i < Count; i++)
        {
            list.Add(new ClassDefault { Id = i, Name = i, Active = true });
        }
        return list;
    }

    [Benchmark]
    public List<ClassSequential> Class_Sequential()
    {
        var list = new List<ClassSequential>(Count);
        for (var i = 0; i < Count; i++)
        {
            list.Add(new ClassSequential { Id = i, Name = i, Active = true });
        }
        return list;
    }

    [Benchmark]
    public List<ClassAuto> Class_Auto()
    {
        var list = new List<ClassAuto>(Count);
        for (var i = 0; i < Count; i++)
        {
            list.Add(new ClassAuto { Id = i, Name = i, Active = true });
        }
        return list;
    }

    [Benchmark]
    public List<StructDefault> Struct_Default()
    {
        var list = new List<StructDefault>(Count);
        for (var i = 0; i < Count; i++)
        {
            list.Add(new StructDefault { Id = i, Name = i, Active = true });
        }
        return list;
    }

    [Benchmark]
    public List<StructSequential> Struct_Sequential()
    {
        var list = new List<StructSequential>(Count);
        for (var i = 0; i < Count; i++)
        {
            list.Add(new StructSequential { Id = i, Name = i, Active = true });
        }
        return list;
    }

    [Benchmark]
    public List<StructAuto> Struct_Auto()
    {
        var list = new List<StructAuto>(Count);
        for (var i = 0; i < Count; i++)
        {
            list.Add(new StructAuto { Id = i, Name = i, Active = true });
        }
        return list;
    }
}

内存对齐后的struct内存分配最少

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值