diff --git a/Content.Benchmarks/EntityFetchBenchmark.cs b/Content.Benchmarks/EntityFetchBenchmark.cs new file mode 100644 index 0000000000..f6016f0525 --- /dev/null +++ b/Content.Benchmarks/EntityFetchBenchmark.cs @@ -0,0 +1,320 @@ +using System; +using System.Collections.Generic; +using BenchmarkDotNet.Attributes; +using Robust.Shared.Utility; + +namespace Content.Benchmarks +{ + [SimpleJob] + public class EntityFetchBenchmark + { + [Params(1000)] public int N { get; set; } + + public int M { get; set; } = 10; + + private readonly DictEntityStorage _dictStorage = new DictEntityStorage(); + private readonly GenEntityStorage _genStorage = new GenEntityStorage(); + + private IEntityStorage _dictStorageInterface; + private IEntityStorage _genStorageInterface; + + private DictEntityUid[] _toReadDict; + private DictEntity[] _toWriteDict; + + private GenEntityUid[] _toReadGen; + private GenEntity[] _toWriteGen; + + [GlobalSetup] + public void Setup() + { + _dictStorageInterface = _dictStorage; + _genStorageInterface = _genStorage; + + var r = new Random(); + + var allocatedGen = new List(); + var allocatedDict = new List(); + + for (var i = 0; i < N; i++) + { + allocatedGen.Add(_genStorage.NewEntity()); + allocatedDict.Add(_dictStorage.NewEntity()); + } + + var delTo = N / 2; + for (var i = 0; i < delTo; i++) + { + var index = r.Next(allocatedDict.Count); + + var gEnt = allocatedGen[index]; + var dEnt = allocatedDict[index]; + + _genStorage.DeleteEntity(gEnt); + _dictStorage.DeleteEntity(dEnt); + + allocatedGen.RemoveSwap(i); + allocatedDict.RemoveSwap(i); + } + + for (var i = 0; i < N; i++) + { + allocatedGen.Add(_genStorage.NewEntity()); + allocatedDict.Add(_dictStorage.NewEntity()); + } + + for (var i = 0; i < delTo; i++) + { + var index = r.Next(allocatedDict.Count); + + var gEnt = allocatedGen[index]; + var dEnt = allocatedDict[index]; + + _genStorage.DeleteEntity(gEnt); + _dictStorage.DeleteEntity(dEnt); + + allocatedGen.RemoveSwap(i); + allocatedDict.RemoveSwap(i); + } + + _toReadDict = new DictEntityUid[M]; + _toWriteDict = new DictEntity[M]; + _toReadGen = new GenEntityUid[M]; + _toWriteGen = new GenEntity[M]; + + for (var i = 0; i < M; i++) + { + var index = r.Next(allocatedDict.Count); + + _toReadDict[i] = allocatedDict[index].Uid; + _toReadGen[i] = allocatedGen[index].Uid; + } + } + + [Benchmark] + public void BenchGenId() + { + for (var i = 0; i < M; i++) + { + var uid = _toReadGen[i]; + if (_genStorage.TryGetEntity(uid, out var entity)) + { + _toWriteGen[i] = entity; + } + } + } + + [Benchmark] + public void BenchDict() + { + for (var i = 0; i < M; i++) + { + var uid = _toReadDict[i]; + if (_dictStorage.TryGetEntity(uid, out var entity)) + { + _toWriteDict[i] = entity; + } + } + } + + [Benchmark] + public void BenchGenIdInterface() + { + for (var i = 0; i < M; i++) + { + var uid = _toReadGen[i]; + if (_genStorageInterface.TryGetEntity(uid, out var entity)) + { + _toWriteGen[i] = entity; + } + } + } + + [Benchmark] + public void BenchDictInterface() + { + for (var i = 0; i < M; i++) + { + var uid = _toReadDict[i]; + if (_dictStorageInterface.TryGetEntity(uid, out var entity)) + { + _toWriteDict[i] = entity; + } + } + } + + private sealed class DictEntityStorage : EntityStorage + { + private int _nextValue; + + private readonly Dictionary _dict = new Dictionary(); + + public override bool TryGetEntity(DictEntityUid entityUid, out DictEntity entity) + { + if (!_dict.TryGetValue(entityUid, out entity)) + { + return false; + } + + return !entity.Deleted; + } + + public DictEntity NewEntity() + { + var e = new DictEntity(new DictEntityUid(_nextValue++)); + _dict.Add(e.Uid, e); + return e; + } + + public void DeleteEntity(DictEntity e) + { + DebugTools.Assert(!e.Deleted); + + e.Deleted = true; + + _dict.Remove(e.Uid); + } + } + + private interface IEntityStorage + { + public bool TryGetEntity(TEntityUid entityUid, out TEntity entity); + } + + private abstract class EntityStorage : IEntityStorage + { + public abstract bool TryGetEntity(TEntityUid entityUid, out TEntity entity); + + public TEntity GetEntity(TEntityUid entityUid) + { + if (!TryGetEntity(entityUid, out var entity)) + { + throw new ArgumentException(); + } + + return entity; + } + } + + private sealed class GenEntityStorage : EntityStorage + { + private (int generation, GenEntity entity)[] _entities = new (int, GenEntity)[1]; + private readonly List _availableSlots = new List {0}; + + public override bool TryGetEntity(GenEntityUid entityUid, out GenEntity entity) + { + var (generation, genEntity) = _entities[entityUid.Index]; + entity = genEntity; + + return generation == entityUid.Generation; + } + + public GenEntity NewEntity() + { + if (_availableSlots.Count == 0) + { + // Reallocate + var oldEntities = _entities; + _entities = new (int, GenEntity)[_entities.Length * 2]; + oldEntities.CopyTo(_entities, 0); + + for (var i = oldEntities.Length; i < _entities.Length; i++) + { + _availableSlots.Add(i); + } + } + + var index = _availableSlots.Pop(); + ref var slot = ref _entities[index]; + var slotEntity = new GenEntity(new GenEntityUid(slot.generation, index)); + slot.entity = slotEntity; + + return slotEntity; + } + + public void DeleteEntity(GenEntity e) + { + DebugTools.Assert(!e.Deleted); + + e.Deleted = true; + + ref var slot = ref _entities[e.Uid.Index]; + slot.entity = null; + slot.generation += 1; + + _availableSlots.Add(e.Uid.Index); + } + } + + + private readonly struct DictEntityUid : IEquatable + { + public readonly int Value; + + public DictEntityUid(int value) + { + Value = value; + } + + public bool Equals(DictEntityUid other) + { + return Value == other.Value; + } + + public override bool Equals(object obj) + { + return obj is DictEntityUid other && Equals(other); + } + + public override int GetHashCode() + { + return Value; + } + + public static bool operator ==(DictEntityUid left, DictEntityUid right) + { + return left.Equals(right); + } + + public static bool operator !=(DictEntityUid left, DictEntityUid right) + { + return !left.Equals(right); + } + } + + private readonly struct GenEntityUid + { + public readonly int Generation; + public readonly int Index; + + public GenEntityUid(int generation, int index) + { + Generation = generation; + Index = index; + } + } + + private sealed class DictEntity + { + public DictEntity(DictEntityUid uid) + { + Uid = uid; + } + + public DictEntityUid Uid { get; } + + public bool Deleted { get; set; } + } + + private sealed class GenEntity + { + public GenEntityUid Uid { get; } + + public bool Deleted { get; set; } + + public GenEntity(GenEntityUid uid) + { + Uid = uid; + } + } + } +}