前言
最近遇到这么一件事,有个函数内存分配很高,想优化一下,因为是纯数据类,因此首先把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更高
| Method | Mean | Error | StdDev | Ratio | RatioSD | Gen0 | Gen1 | Allocated | Alloc Ratio |
|---|---|---|---|---|---|---|---|---|---|
| Class_Default | 640.8 ns | 10.57 ns | 9.89 ns | 1.00 | 0.00 | 1.1559 | 0.0305 | 7.09 KB | 1.00 |
| Struct_Default | 605.7 ns | 4.60 ns | 4.08 ns | 0.95 | 0.02 | 1.2836 | 0.0372 | 7.87 KB | 1.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;
}

完整测试代码和结果
| Method | Mean | Error | StdDev | Ratio | RatioSD | Gen0 | Gen1 | Allocated | Alloc Ratio |
|---|---|---|---|---|---|---|---|---|---|
| Class_Default | 715.2 ns | 14.03 ns | 13.12 ns | 1.00 | 0.00 | 1.1559 | 0.0305 | 7.09 KB | 1.00 |
| Class_Sequential | 814.2 ns | 15.59 ns | 19.14 ns | 1.13 | 0.03 | 1.6661 | 0.0639 | 10.21 KB | 1.44 |
| Class_Auto | 690.6 ns | 12.98 ns | 12.14 ns | 0.97 | 0.03 | 1.1559 | 0.0305 | 7.09 KB | 1.00 |
| Struct_Default | 682.8 ns | 13.31 ns | 13.07 ns | 0.96 | 0.03 | 1.2836 | 0.0372 | 7.87 KB | 1.11 |
| Struct_Sequential | 677.9 ns | 9.16 ns | 8.12 ns | 0.95 | 0.02 | 1.2836 | 0.0372 | 7.87 KB | 1.11 |
| Struct_Auto | 462.6 ns | 8.75 ns | 8.60 ns | 0.65 | 0.02 | 0.7739 | 0.0138 | 4.74 KB | 0.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内存分配最少
本文探讨在C#中将纯数据class改为struct后内存占用不降反升的现象,核心原因在于struct默认采用Sequential布局导致内存对齐不足,而class默认Auto布局由CLR自动优化字段排列以减少填充。通过StructLayout显式指定Explicit或Sequential并配合FieldOffset可改善内存布局,实测显示合理内存对齐后struct可降至最小内存占用。关键涉及StructLayout、LayoutKind、字段重排、内存填充与CLR内存管理机制。
132

被折叠的 条评论
为什么被折叠?



