Skip to content

Instantly share code, notes, and snippets.

@sebas77
Last active February 19, 2022 18:37
Show Gist options
  • Save sebas77/11d1442677bce0bbf0d5d0024b99abca to your computer and use it in GitHub Desktop.
Save sebas77/11d1442677bce0bbf0d5d0024b99abca to your computer and use it in GitHub Desktop.
ConcurrentDictionary allocations make it bad for games (at least with Unity GC)
using System.Collections.Concurrent;
using System.Runtime.InteropServices;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
class Program
{
static void Main(string[] args)
{
BenchmarkRunner.Run<Benchmarks>();
}
}
[MemoryDiagnoser] // we need to enable it in explicit way
public class Benchmarks
{
[Serializable]
[StructLayout(LayoutKind.Explicit)]
public struct EGID : IEquatable<EGID>, IComparable<EGID>
{
[FieldOffset(0)] public readonly uint entityID;
[FieldOffset(4)] public readonly uint groupID;
[FieldOffset(0)] readonly ulong _GID;
public static bool operator ==(EGID obj1, EGID obj2)
{
return obj1._GID == obj2._GID;
}
public static bool operator !=(EGID obj1, EGID obj2)
{
return obj1._GID != obj2._GID;
}
static ulong MAKE_GLOBAL_ID(uint entityId, uint groupId)
{
return (ulong)groupId << 32 | ((ulong)entityId & 0xFFFFFFFF);
}
public static explicit operator uint(EGID id)
{
return id.entityID;
}
//in the way it's used, ulong must be always the same for each id/group
public static explicit operator ulong(EGID id)
{
return id._GID;
}
public bool Equals(EGID other)
{
return _GID == other._GID;
}
public bool Equals(EGID x, EGID y)
{
return x == y;
}
public override int GetHashCode()
{
return _GID.GetHashCode();
}
public int GetHashCode(EGID egid)
{
return egid.GetHashCode();
}
public int CompareTo(EGID other)
{
return _GID.CompareTo(other._GID);
}
internal EGID(uint entityID, uint groupID) : this()
{
_GID = MAKE_GLOBAL_ID(entityID, groupID);
}
}
/// <summary>
/// Unfortunately this must be and stay a class
/// </summary>
public class RefType : IDisposable
{
public uint dataPos { get; set; }
public void Dispose()
{
throw new NotImplementedException();
}
}
ConcurrentDictionary<uint, ValueType> _concurrentDictionary;
ConcurrentDictionary<EGID, RefType> _concurrentDictionary2;
[GlobalSetup]
public void GlobalSetup()
{
_concurrentDictionary = new ConcurrentDictionary<uint, ValueType>(8, 10);
_concurrentDictionary[0] = default;
_concurrentDictionary[1] = default;
_concurrentDictionary[2] = default;
_concurrentDictionary2 = new ConcurrentDictionary<EGID, RefType>(8, 10);
_concurrentDictionary2[new EGID(0, 0)] = default;
_concurrentDictionary2[new EGID(1, 0)] = default;
_concurrentDictionary2[new EGID(2, 0)] = default;
}
[Benchmark] //this allocates
public void TestUnexpectedAllocationsWithConcurrentDictionarySimple()
{
_concurrentDictionary[0] = default;
}
[Benchmark] //this allocates
public void TestUnexpectedAllocationsWithConcurrentDictionary()
{
_concurrentDictionary[0] = default;
_concurrentDictionary[1] = default;
_concurrentDictionary[2] = default;
}
[Benchmark] //this does not allocate
public void TestUnexpectedAllocationsWithConcurrentDictionary2Simple()
{
_concurrentDictionary2[new EGID(0, 0)] = default;
}
[Benchmark] //this does not allocate
public void TestUnexpectedAllocationsWithConcurrentDictionary2()
{
_concurrentDictionary2[new EGID(0, 0)] = default;
_concurrentDictionary2[new EGID(1, 0)] = default;
_concurrentDictionary2[new EGID(2, 0)] = default;
}
public struct ValueType : IEquatable<ValueType>
{
public float currentPriority;
public uint lastSentFrame;
public bool Equals(ValueType other)
{
return currentPriority == other.currentPriority && lastSentFrame == other.lastSentFrame;
}
}
}
@sebas77
Copy link
Author

sebas77 commented Feb 19, 2022

image

@sebas77
Copy link
Author

sebas77 commented Feb 19, 2022

ok I think I finally understood, ConcurrentDictionary for some reason I can't understand, allocates when the value is a ValueType. For the Key it's instead irrelevant (at least for small ones)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment