Skip to content

Instantly share code, notes, and snippets.

@laicasaane
Last active December 6, 2021 09:22
Show Gist options
  • Save laicasaane/126b6e3104f29e6828e16611c27e1e0f to your computer and use it in GitHub Desktop.
Save laicasaane/126b6e3104f29e6828e16611c27e1e0f to your computer and use it in GitHub Desktop.
Test GC Alloc for struct to interface casting
using System;
using UnityEngine;
namespace Examples
{
public partial class ExampleBehaviour : MonoBehaviour
{
private static readonly UnityEngine.Profiling.Recorder s_recorder;
static ExampleBehaviour()
{
s_recorder = UnityEngine.Profiling.Recorder.Get("GC.Alloc");
}
private static int CountGCAllocs(Action action)
{
s_recorder.FilterToCurrentThread();
s_recorder.enabled = false;
s_recorder.enabled = true;
action();
s_recorder.enabled = false;
return s_recorder.sampleBlockCount;
}
private void Start()
{
EGIDSetter<ComponentA>.Warmup();
CountGCAllocs(SetIDWithoutCasting);
var count = CountGCAllocs(SetIDWithoutCasting);
Debug.Log($"Set Without Casting: {count}");
count = CountGCAllocs(SetIDWithCasting);
Debug.Log($"Set With Casting: {count}");
}
private void SetIDWithoutCasting()
{
var comp = new ComponentA();
for (var i = 1; i <= 1_000_000; i++)
{
var id = new EGID(i);
EGIDSetter<ComponentA>.SetIDWithoutCasting(ref comp, id);
}
}
private void SetIDWithCasting()
{
var comp = new ComponentA();
for (var i = 1; i <= 1_000_000; i++)
{
var id = new EGID(i);
EGIDSetter<ComponentA>.SetIDWithCasting(ref comp, id);
}
}
private struct ComponentA : IEntityComponent, INeedEGID
{
public EGID ID { get; set; }
public override string ToString()
=> ID.ToString();
}
}
public struct EGID
{
private readonly int _value;
public EGID(int value)
=> _value = value;
public override string ToString()
=> _value.ToString();
}
public interface IEntityComponent { }
public interface INeedEGID
{
EGID ID { get; set; }
}
delegate void SetEGIDAction<T>(ref T target, EGID egid) where T : struct, IEntityComponent;
static class EGIDSetter<T> where T : struct, IEntityComponent
{
public static readonly SetEGIDAction<T> SetIDWithoutCasting = MakeSetter();
public static void Warmup() { }
static SetEGIDAction<T> MakeSetter()
{
var method = typeof(Trick).GetMethod(nameof(Trick.SetIDWihoutCasting)).MakeGenericMethod(typeof(T));
return (SetEGIDAction<T>)System.Delegate.CreateDelegate(typeof(SetEGIDAction<T>), method);
}
public static void SetIDWithCasting(ref T target, EGID egid)
{
if (target is INeedEGID needEGID)
{
needEGID.ID = egid;
target = (T)needEGID;
}
}
static class Trick
{
public static void SetIDWihoutCasting<U>(ref U target, EGID egid) where U : struct, INeedEGID
{
target.ID = egid;
}
}
}
}
@laicasaane
Copy link
Author

laicasaane commented Dec 5, 2021

Change the method of GC Alloc inspection
https://docs.microsoft.com/en-us/dotnet/api/system.gc.gettotalmemory?view=net-6.0

using System;
using System.Collections.Generic;
using UnityEngine;

namespace Examples
{
    ///vvvvvvvvvvvvvvvvvvvvvvvvvvv
    /// Components
    ///vvvvvvvvvvvvvvvvvvvvvvvvvvv

    public struct ComponentA : IEntityComponent, INeedEGID
    {
        public EGID ID { get; set; }

        public override string ToString()
            => ID.ToString();
    }

    public struct EGID
    {
        private readonly int _value;

        public EGID(int value)
            => _value = value;

        public override string ToString()
            => _value.ToString();
    }

    public interface IEntityComponent { }

    public interface INeedEGID
    {
        EGID ID { get; set; }
    }

    ///vvvvvvvvvvvvvvvvvvvvvvvvvvv
    /// EGIDSetter
    ///vvvvvvvvvvvvvvvvvvvvvvvvvvv

    delegate void SetEGIDAction<T>(ref T target, EGID egid) where T : struct, IEntityComponent;

    static class EGIDSetter<T> where T : struct, IEntityComponent
    {
        public static readonly SetEGIDAction<T> SetIDWithoutCasting = MakeSetter();

        public static void Warmup() { }

        static SetEGIDAction<T> MakeSetter()
        {
            var method = typeof(Trick).GetMethod(nameof(Trick.SetIDWihoutCasting)).MakeGenericMethod(typeof(T));
            return (SetEGIDAction<T>)System.Delegate.CreateDelegate(typeof(SetEGIDAction<T>), method);
        }

        public static void SetIDWithCasting(ref T target, EGID egid)
        {
            if (target is INeedEGID needEGID)
            {
                needEGID.ID = egid;
                target = (T)needEGID;
            }
        }

        static class Trick
        {
            public static void SetIDWihoutCasting<U>(ref U target, EGID egid) where U : struct, INeedEGID
            {
                target.ID = egid;
            }
        }
    }

    ///vvvvvvvvvvvvvvvvvvvvvvvvvvv
    /// The Test starts here
    ///vvvvvvvvvvvvvvvvvvvvvvvvvvv

    public partial class ExampleBehaviour : MonoBehaviour
    {
        private void Start()
        {
            EGIDSetter<ComponentA>.Warmup();
        }

        private void Update()
        {
            if (Input.GetKeyDown(KeyCode.Alpha1)
                || Input.GetKeyDown(KeyCode.Keypad1))
            {
                RunTest(false, false);
            }

            if (Input.GetKeyDown(KeyCode.Alpha2)
                || Input.GetKeyDown(KeyCode.Keypad2))
            {
                RunTest(true, false);
            }

            if (Input.GetKeyDown(KeyCode.Alpha3)
                || Input.GetKeyDown(KeyCode.Keypad3))
            {
                RunTest(true, true);
            }
        }

        private void RunTest(bool runT2, bool runT3)
        {
            RunTest(1024, runT2, runT3);

            Debug.Log("");
            GC.Collect(0);

            RunTest(1_048_576, runT2, runT3);
            GC.Collect(0);

            RunTestMultipleTimes(10, 1024, runT2, runT3);
            RunTestMultipleTimes(10, 1_048_576, runT2, runT3);
        }

        private void RunTestMultipleTimes(int count, int max, bool runT2, bool runT3)
        {
            Debug.LogError($"Run test {count} times");
            GC.Collect(0);

            var t1 = 0L;
            var t2 = 0L;
            var t3 = 0L;

            for (var i = 0; i < count; i++)
            {
                GC.Collect(0);
                var (x1, x2, x3) = RunTest(max, runT2, runT3, false);
                t1 += x1;
                t2 += x2;
                t3 += x3;
            }

            var avg1 = t1 / count;
            var avg2 = t2 / count;
            var avg3 = t3 / count;

            Debug.LogWarning($"SetID_NoBoxing: x{max}");
            Debug.Log($"Average increase: {avg1}");
            Debug.LogWarning($"SetID_Boxing: x{max}");
            Debug.Log($"Average increase: {avg2}");
            Debug.LogWarning($"MakeSomeGarbage: {max} objects of System.Version");
            Debug.Log($"Average increase: {avg3}");
        }

        private (long t1, long t2, long t3) RunTest(int max, bool runT2, bool runT3, bool canLog = true)
        {
            var gcTest = new GCTest {
                Max = max
            };

            var gen0 = GC.GetGeneration(gcTest);
            var gc0 = GC.GetTotalMemory(false);

            gcTest.SetIDWithoutCasting();

            var gen1 = GC.GetGeneration(gcTest);
            var gc1 = GC.GetTotalMemory(false);
            var diff1 = gc1 - gc0;
            //GC.Collect(gen1);

            long gen2 = 0;
            long diff2 = 0;
            long gc2 = gc0;

            if (runT2)
            {
                gcTest.SetIDWithCasting();

                gen2 = GC.GetGeneration(gcTest);
                gc2 = GC.GetTotalMemory(false);
                diff2 = gc2 - gc1;
            }

            long gen3 = 0;
            long gc3 = 0;
            long diff3 = 0;

            if (runT3)
            {
                var versions = gcTest.MakeSomeGarbage();

                gen3 = GC.GetGeneration(gcTest);
                gc3 = GC.GetTotalMemory(false);
                diff3 = gc3 - gc2;
            }

            if (canLog)
            {
                Debug.LogWarning("Initial");
                Debug.Log($"Gen: {gen0} | Total Memory #0: {gc0}");
                Debug.LogWarning($"SetID_NoBoxing: x{max}");
                Debug.Log($"Gen: {gen1} | Total Memory #1: {gc1} | Increase: {diff1}");

                if (runT2)
                {
                    Debug.LogWarning($"SetID_Boxing: x{max}");
                    Debug.Log($"Gen: {gen2} | Total Memory #2: {gc2} | Increase: {diff2}");
                }

                if (runT3)
                {
                    Debug.LogWarning($"MakeSomeGarbage: {max} objects of System.Version");
                    Debug.Log($"Gen: {gen3} | Total Memory #3: {gc3} | Increase: {diff3}");
                }
            }

            return (diff1, diff2, diff3);
        }

        private class GCTest
        {
            public int Max { get; set; }

            public void SetIDWithoutCasting()
            {
                var comp = new ComponentA();

                for (var i = 1; i <= Max; i++)
                {
                    var id = new EGID(i);
                    EGIDSetter<ComponentA>.SetIDWithoutCasting(ref comp, id);
                }
            }

            public void SetIDWithCasting()
            {
                var comp = new ComponentA();

                for (var i = 1; i <= Max; i++)
                {
                    var id = new EGID(i);
                    EGIDSetter<ComponentA>.SetIDWithCasting(ref comp, id);
                }
            }

            public List<Version> MakeSomeGarbage()
            {
                var versions = new List<Version>(Max);

                for (var i = 1; i <= Max; i++)
                {
                    versions.Add(new Version());
                }

                return versions;
            }
        }
    }
}

@laicasaane
Copy link
Author

laicasaane commented Dec 5, 2021

The result of this method clearly show that casting a struct to interface will incur boxing at runtime.

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