处理百万级实体:选择Class还是Struct更合适?
应用场景
- 内存中处理百万级甚至更多实体
- 在无限循环中处理(每轮最多1ms)
- 快速加载和保存至二进制文件
- 大量以实体为参数的链式方法调用
无法实例化所有实体,分配成本过高,计划将其加载至byte[]数组,按需使用。
Class方案
public class Entity : IDisposable { public Pool Pool { get; } private readonly int _size; private int _id; private int _offset; private readonly Property[] _properties; private readonly byte[] _tape; public int Id { get => _id; set { _id = value; _offset = value * _size; } } public int Value1 { get => GetInt32(0); set => SetInt32(0, value); } public int Value2 { get => GetInt32(1); set => SetInt32(1, value); } public Entity(Pool pool) { Pool = pool; _size = pool.Schema.Size; _properties = pool.Schema.GetProperties(); _tape = pool.Tape; } public int GetInt32(int index) { var offset = GetOffset(index); return _tape[offset] << 24 | _tape[offset + 1] << 16 | _tape[offset + 2] << 8 | _tape[offset + 3]; } public void SetInt32(int index, int value) { var offset = GetOffset(index); _tape[offset] = (byte)(value >> 24); _tape[offset + 1] = (byte)(value >> 16); _tape[offset + 2] = (byte)(value >> 8); _tape[offset + 3] = (byte)value; } private int GetOffset(int index) { var property = _properties[index]; return _offset + property.Offset; } public void Dispose() { Pool.Release(this); } }
使用示例:
while(true) { using(var entity = pool.Lease()) { entity.Id = 123; entity.Value1 = entity.Value2 + 123; DoSomething1(entity); DoSomething2(entity); } }
Struct方案
public struct Entity { public Pool Pool { get; } private readonly int _size; private int _id; private int _offset; private readonly Property[] _properties; private readonly byte[] _tape; public int Id { get => _id; set { _id = value; _offset = value * _size; } } public int Value1 { get => GetInt32(0); set => SetInt32(0, value); } public int Value2 { get => GetInt32(1); set => SetInt32(1, value); } public Entity(Pool pool) { Pool = pool; _size = pool.Schema.Size; _properties = pool.Schema.GetProperties(); _tape = pool.Tape; } public int GetInt32(int index) { var offset = GetOffset(index); return _tape[offset] << 24 | _tape[offset + 1] << 16 | _tape[offset + 2] << 8 | _tape[offset + 3]; } public void SetInt32(int index, int value) { var offset = GetOffset(index); _tape[offset] = (byte)(value >> 24); _tape[offset + 1] = (byte)(value >> 16); _tape[offset + 2] = (byte)(value >> 8); _tape[offset + 3] = (byte)value; } private int GetOffset(int index) { var property = _properties[index]; return _offset + property.Offset; } }
使用示例:
while(true) { var entity = new Entity(pool); entity.Id = 123; entity.Value1 = entity.Value2 + 123; DoSomething1(entity); DoSomething2(entity); }
问题解答
哪种方案更优?
结合你的场景,Struct方案更适合,但要注意值类型带来的潜在陷阱;Class方案仅在需要多态、接口支持或复杂对象生命周期管理时才有优势,在你的高频率、低延迟需求下,Struct的性能表现更适配。
该场景下需要考虑的核心因素
内存分配与GC压力
- Class是引用类型,即使依赖对象池,池为空时仍需在堆上分配新实例,频繁创建回收会触发GC停顿,直接影响每轮1ms的时间限制。
- Struct是值类型,默认在栈上分配,创建实例几乎无分配成本,不会产生GC压力,完美契合无限循环的低延迟要求。
性能与调用开销
- Struct的成员访问是直接栈内存操作,比Class的堆内存间接访问更快,百万级实体处理场景下,这种微小差距会被放大。
- 注意:如果Struct作为参数传递时未用
ref/in修饰,会发生值拷贝。若Struct体积较大(比如包含多个引用字段),拷贝成本会上升,建议用ref传递避免拷贝,例如DoSomething1(ref entity)。
对象池的必要性
- Class方案依赖对象池规避分配,但池本身有管理开销(锁竞争、空闲链表维护),极高并发场景下可能成为瓶颈。
- Struct方案无需对象池,直接创建即可,省去池的管理成本,更适合无限循环场景。
装箱风险
- 若将Struct传递给接受
object或接口类型的方法,会触发装箱操作,导致堆分配和GC。需确保所有业务方法直接接受Struct类型,或用in/ref修饰符避免装箱。
- 若将Struct传递给接受
状态一致性
- Struct是值类型,拷贝后为独立实例,方法中修改拷贝后的Struct不会影响原实例状态。需确保业务逻辑不存在依赖引用传递的场景,避免出现意外行为。
字段初始化与不可变性
- Struct的只读字段必须在构造函数中初始化,你的方案中
_size、_properties、_tape均为只读,符合从Pool获取固定值的场景需求,无需额外调整。
- Struct的只读字段必须在构造函数中初始化,你的方案中
总结
优先选择Struct方案,同时优化方法参数传递(用ref/in避免拷贝)、规避装箱操作,就能最大化性能并消除GC压力。如果后续需要支持多态或接口,再考虑Class+对象池方案,但要做好池的性能优化(比如无锁设计)。
内容的提问来源于stack exchange,提问作者user1576055
相关产品推荐
相关产品推荐

