SerializeReference performence

Is there anyone who tried to measure real performance of SerializeReference attribute?
Documentation states it is slower, sure but how much slower - like 2x times slower or 100x slower?

By-value serialization is more efficient than using SerializeReference in terms of storage, memory, and loading and saving time, so you should only use SerializeReference in situations which require it.

I wonder if there is overhead only when asset is loaded from disk, or it also causes slowdowns when Instantiate() is used to create copy of object using that attribute.

It’s considerably slower. Instantiating an Object that used [SerializeReference] over [SerializeField] increased its overall instantiation time to 360%, the last time I measured it. I can’t remember for sure the exact number of fields each Object had in the test, but I think it was five or six.

There are people who avoid DI frameworks that use reflection during initialization like the plague because they think they’re too slow - but I wonder how many of them still happily use [SerializeReference] all the time, not realizing that it’s way slower.

Edit: I found the test, and it was six fields per Object.

4 Likes

Thank you, this gives me some insight!
By 6 fields you mean any 6 in total or all 6 with [SerializeReference]?

I though about measuring if it’s more of constant overhead or it scales with number of objects - I mean by this that there is one array/list field in component and if the fact [SerializeReference] is there it will be just slower or it scales strongly with number of objects.

Am I the only one who actually loads games? Did I miss a meeting or something?

No warm up and of course in editor:


using System.Diagnostics;
using UnityEngine;

[System.Serializable]
public class TestObject
{
    public int field1;
    public int field2;
    public int field3;
    public int field4;
    public int field5;
}

public class Test : MonoBehaviour
{
    int n = 100000;
    [SerializeReference] private TestObject referenceObject = new TestObject();
    [SerializeField] private TestObject regularObject = new TestObject();

    void Start()
    {
        Stopwatch stopwatch;

        stopwatch = Stopwatch.StartNew();
        for (int i = 0; i < n; i++)
        {
            JsonUtility.ToJson(regularObject);
        }
        stopwatch.Stop();
        UnityEngine.Debug.Log($"Serialization (SerializeField) took: {stopwatch.ElapsedMilliseconds} ms for {n} iterations.");

        stopwatch = Stopwatch.StartNew();
        for (int i = 0; i < n; i++)
        {
            JsonUtility.ToJson(referenceObject);
        }
        stopwatch.Stop();
        UnityEngine.Debug.Log($"Serialization (SerializeReference) took: {stopwatch.ElapsedMilliseconds} ms for {n} iterations.");

        string regularJson = JsonUtility.ToJson(regularObject);
        string referenceJson = JsonUtility.ToJson(referenceObject);

        stopwatch = Stopwatch.StartNew();
        for (int i = 0; i < n; i++)
        {
            JsonUtility.FromJson<TestObject>(regularJson);
        }
        stopwatch.Stop();
        UnityEngine.Debug.Log($"Deserialization (SerializeField) took: {stopwatch.ElapsedMilliseconds} ms for {n} iterations.");

        stopwatch = Stopwatch.StartNew();
        for (int i = 0; i < n; i++)
        {
            JsonUtility.FromJson<TestObject>(referenceJson);
        }
        stopwatch.Stop();
        UnityEngine.Debug.Log($"Deserialization (SerializeReference) took: {stopwatch.ElapsedMilliseconds} ms for {n} iterations.");
    }
}

And just creating:


using System.Diagnostics;
using UnityEngine;

[System.Serializable]
public class TestObject
{
    public int field1;
    public int field2;
    public int field3;
    public int field4;
    public int field5;
    public int field6;
}

public class Test : MonoBehaviour
{
    int n = 10000000;
    [SerializeReference] private TestObject referenceObject;
    [SerializeField] private TestObject regularObject;

    void Start()
    {
        Stopwatch stopwatch;

        stopwatch = Stopwatch.StartNew();
        for (int i = 0; i < n; i++)
        {
            regularObject = new TestObject();
        }
        stopwatch.Stop();
        UnityEngine.Debug.Log($"Instantiation (SerializeField) took: {stopwatch.ElapsedMilliseconds} ms for {n} iterations.");

        stopwatch = Stopwatch.StartNew();
        for (int i = 0; i < n; i++)
        {
            referenceObject = new TestObject();
        }
        stopwatch.Stop();
        UnityEngine.Debug.Log($"Instantiation (SerializeReference) took: {stopwatch.ElapsedMilliseconds} ms for {n} iterations.");
    }
}

Modify my code if you see any errors I’d like to have an update.

I feel like that’s a flawed test as the data isn’t really taking advantage of what you’d use [SerializeReference] for in the first place, which is polymorphism and non-linear data structures.

To that end I don’t think there’s a fair test you can really do between [SerializeField] and [SerializeReference] as they’re intended for two completely different use cases.

You can’t test how fast one deserialises a tree-like data structure, for example, because, well, you can’t do that with [SerializeField] short of using a bunch of scriptable objects. But why would you be doing that in the first place?

And for a large collection of data without polymorphism, why would you be using [SerializeReference]? (Though it can be done with the expectation of introducing polymorphism later on).

The yaml does pile up when using lots of [SerializeReference], substantially more so than when using [SerializeField]. And I highly imagine there’s a correllation between more data and longer deserialization/load times.

5 Likes

I tested it, and the results were mostly the same as @SisusCo’s comment.

TestSerializeObject.cs

using UnityEngine;

[System.Serializable]
public class TestSerializeObject
{
    public int i0;
    public int i1;
    public int i2;
    public int i3;
    public int i4;
    public int i5;
}

SerializeFieldBehaviour.cs

using UnityEngine;

public class SerializeFieldBehaviour : MonoBehaviour
{
    [SerializeField] TestSerializeObject o0;
    [SerializeField] TestSerializeObject o1;
    [SerializeField] TestSerializeObject o2;
    [SerializeField] TestSerializeObject o3;
    [SerializeField] TestSerializeObject o4;
    [SerializeField] TestSerializeObject o5;

#if UNITY_EDITOR
    // Call this method when creating a prefab in the editor to initialize the values.
    [UnityEditor.MenuItem("CONTEXT/SerializeFieldBehaviour/Initialize SerializeField")]
    static void InitializeSerializeField(UnityEditor.MenuCommand command)
    {
        var behaviour = command.context as SerializeFieldBehaviour;
        Debug.Assert(behaviour != null);

        behaviour.o0 = new() { i0 = 0, i1 = 1, i2 = 2, i3 = 3, i4 = 4, i5 = 5 };
        behaviour.o1 = new() { i0 = 0, i1 = 1, i2 = 2, i3 = 3, i4 = 4, i5 = 5 };
        behaviour.o2 = new() { i0 = 0, i1 = 1, i2 = 2, i3 = 3, i4 = 4, i5 = 5 };
        behaviour.o3 = new() { i0 = 0, i1 = 1, i2 = 2, i3 = 3, i4 = 4, i5 = 5 };
        behaviour.o4 = new() { i0 = 0, i1 = 1, i2 = 2, i3 = 3, i4 = 4, i5 = 5 };
        behaviour.o5 = new() { i0 = 0, i1 = 1, i2 = 2, i3 = 3, i4 = 4, i5 = 5 };

        UnityEditor.EditorUtility.SetDirty(behaviour);
    }
#endif
}

SerializeReferenceBehaviour.cs

using UnityEngine;

public class SerializeReferenceBehaviour : MonoBehaviour
{
    [SerializeReference] TestSerializeObject o0;
    [SerializeReference] TestSerializeObject o1;
    [SerializeReference] TestSerializeObject o2;
    [SerializeReference] TestSerializeObject o3;
    [SerializeReference] TestSerializeObject o4;
    [SerializeReference] TestSerializeObject o5;

#if UNITY_EDITOR
    // Call this method when creating a prefab in the editor to initialize the values.
    [UnityEditor.MenuItem("CONTEXT/SerializeReferenceBehaviour/Initialize SerializeReference")]
    static void InitializeSerializeReference(UnityEditor.MenuCommand command)
    {
        var behaviour = command.context as SerializeReferenceBehaviour;
        Debug.Assert(behaviour != null);

        behaviour.o0 = new() { i0 = 0, i1 = 1, i2 = 2, i3 = 3, i4 = 4, i5 = 5 };
        behaviour.o1 = new() { i0 = 0, i1 = 1, i2 = 2, i3 = 3, i4 = 4, i5 = 5 };
        behaviour.o2 = new() { i0 = 0, i1 = 1, i2 = 2, i3 = 3, i4 = 4, i5 = 5 };
        behaviour.o3 = new() { i0 = 0, i1 = 1, i2 = 2, i3 = 3, i4 = 4, i5 = 5 };
        behaviour.o4 = new() { i0 = 0, i1 = 1, i2 = 2, i3 = 3, i4 = 4, i5 = 5 };
        behaviour.o5 = new() { i0 = 0, i1 = 1, i2 = 2, i3 = 3, i4 = 4, i5 = 5 };

        UnityEditor.EditorUtility.SetDirty(behaviour);
    }
#endif
}

SerializePerformanceTester.cs

using UnityEngine;

public class SerializePerformanceTester : MonoBehaviour
{
    const int testNum = 100000;

    public bool testSerializeField;
    public SerializeFieldBehaviour serializeFieldPrefab;
    public SerializeReferenceBehaviour serializeReferencePrefab;

    void Start()
    {
        if (testSerializeField)
            Debug.Log("Test SerializeField");
        else
            Debug.Log("Test SerializeReference");

        var sw = System.Diagnostics.Stopwatch.StartNew();
        if (testSerializeField)
        {
            for (int i = 0; i < testNum; i++)
                Instantiate(serializeFieldPrefab);
        }
        else
        {
            for (int i = 0; i < testNum; i++)
                Instantiate(serializeReferencePrefab);
        }
        sw.Stop();
        Debug.Log($"Elapsed : {sw.Elapsed}");
    }
}

Test results

Backend SerializeField (sec) SerializeReference (sec)
Editor 3.8213440 10.1807955
Mono 1.1837976 3.9963540
IL2CPP 0.9641599 2.9475725

A test in the editor with a single field in the above code (i.e., removing o1 to o5).

Backend SerializeField (sec) SerializeReference (sec)
Editor 3.3694590 5.5051361
3 Likes

The instantiated GameObject had a single component attached to it (in addition to the Transform, of course), and that single component had six serialized fields.

1 Like

Actually I also tried to run very similar test in editor, but instead of fields there is one array.
First one is obviously serialized by class name and the other one by interface with 3 different objects, one with single object field, one with enums and last one has no fields.

Elements in array SerializeField SerializeReference
0 4.192 4.443
1 4.409 6.777
4 4.761 9.236
20 6.936 23.798
50 9.803 50.009
100 18.400 1:33.658

I am curious now if there is huge difference in build, because data is serialized differently and most likely there are some other optimizations.

Yes, non-development builds can give very different results than the editor in some cases. Profiling like this should optimally always be done in builds.

I ran a new benchmark using Unity 6 and a non-development build. Pretty similar results this time around as well:

Tested Total (s) Average (ns)
No Injection (Baseline) 1.664 1.664
Singleton 1.845 1.845
[SerializeField] 1.904 1.904
Init(args) 3.053 3.053
[SerializeReference] 8.312 8.312
Extenject (Reflection Mode) 8.718 8.718

The test was for Instantiate x 1,000,000 with six dependencies per client.

All tests used Object references, including [SerializeField], except for [SerializeReference], which used small plain old C# objects assigned into interface type fields.

Test Code
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using Sisus.Init;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.Scripting;
using Action = System.Action;
using Debug = UnityEngine.Debug;

namespace Sisus.PerformanceTests
{
	public class PerformanceTests : MonoBehaviour
	{
		const int WarmupCount = 10;
		const int MeasurementCount = 10_000;
		private const int IterationsPerMeasurement = 100;
		
		private async void Awake()
		{
			DontDestroyOnLoad(gameObject);

			for(int i = MeasurementCount - 1; i >= 0; i--)
			{
				var tasks = new[]
				{
					No_Injection(),
					InitArgs(),
					Extenject(),
					SerializeField(),
					SerializeReference(),
					Singleton()
				};

				// Shuffle the tasks to avoid results being skewed due to order
				foreach(var task in tasks.OrderBy(a => Guid.NewGuid()))
				{
					await task;
				}
			}

			Debug.Log("\n---------------------------------\n");
			Debug.Log("InstantiateX" + (MeasurementCount * IterationsPerMeasurement) + "\n"
                     + "Test,Average(ns),Total(s)\n"
                     + string.Join("\n", Results.all.OrderBy(x => x.stopwatch.ElapsedMilliseconds)));
			Debug.Log("\n----------------------------------\n");
			Application.Quit();
		}

		public Task No_Injection()
		{
			var original = Resources.Load<NoInjection>("NoInjection");
			return Measure.Method(() => Instantiate(original));
		}
		
		public async Task InitArgs()
		{
			var original = Resources.Load<MonoBehaviourT>("MonoBehaviourT");
			var arg1 = Resources.Load<Arg1>("Arg1");
			var arg2 = Resources.Load<Arg2>("Arg2");
			var arg3 = Resources.Load<Arg3>("Arg3");
			var arg4 = Resources.Load<Arg4>("Arg4");
			var arg5 = Resources.Load<Arg5>("Arg5");
			var arg6 = Resources.Load<Arg6>("Arg6");
			Service.Set(arg1);
			Service.Set(arg2);
			Service.Set(arg3);
			Service.Set(arg4);
			Service.Set(arg5);
			Service.Set(arg6);

			await Measure.Method(() => Instantiate(original));

			Service.Unset(arg1);
			Service.Unset(arg2);
			Service.Unset(arg3);
			Service.Unset(arg4);
			Service.Unset(arg5);
			Service.Unset(arg6);
		}
		
		public Task Extenject()
		{
			var original = Resources.Load<Extenject>("Extenject");
			return Measure.Method(() => Instantiate(original));
		}

		public Task SerializeField()
		{
			var original = Resources.Load<SerializeField>("SerializeField");
			return Measure.Method(() => Instantiate(original));
		}

		public Task SerializeReference()
		{
			var original = Resources.Load<SerializeReference>("SerializeReference");
			return Measure.Method(() => Instantiate(original));
		}

		public Task Singleton()
		{
			var original = Resources.Load<Singleton>("Singleton");
			return Measure.Method(() => Instantiate(original));
		}

		class Results
		{
			internal static List<Results> all = new(16);
			
			public string method;
			public Stopwatch stopwatch;
			public int totalIterations;

			public static Stopwatch BeginMeasurement(string method)
			{
				foreach(var result in all)
				{
					if(result.method == method)
					{
						result.totalIterations += IterationsPerMeasurement;
						return result.stopwatch;
					}
				}
				
				var newResult = new Results { method = method, stopwatch = new(), totalIterations = IterationsPerMeasurement };
				all.Add(newResult);
				return newResult.stopwatch;
			}

			public override string ToString()
			{
				long totalMs = stopwatch.ElapsedMilliseconds;
				var averageNs = (1000d * ((double)totalMs / totalIterations)).ToString("0.####");
				var totalS = (totalMs / 1000d).ToString("0.####");
				return method + ",average:" + averageNs + ",total:" + totalS;
			}
		}

		public class Measure
		{
			public static Task Method(Action action, [CallerMemberName] string method = null) => Method(null, action, method);
			public static async Task Method(Action oneTimeSetup, Action action, [CallerMemberName] string method = null)
			{
				var activeSceneWas = SceneManager.GetActiveScene();
				var tempScene = SceneManager.CreateScene(Guid.NewGuid().ToString());
				while(!tempScene.isLoaded)
				{
					await Awaitable.NextFrameAsync();
				}

				SceneManager.SetActiveScene(tempScene);

				oneTimeSetup?.Invoke();

				for(int i = 0; i < WarmupCount; i++)
				{
					action();
				}

				var stopwatch = Results.BeginMeasurement(method);

				stopwatch.Start();

				for(int i = IterationsPerMeasurement - 1; i >= 0; i--)
				{
					action();
				}

				stopwatch.Stop();

				if(!activeSceneWas.isLoaded)
				{
					activeSceneWas = SceneManager.CreateScene(Guid.NewGuid().ToString());
					while(!activeSceneWas.isLoaded)
					{
						await Awaitable.NextFrameAsync();
					}
				}

				SceneManager.SetActiveScene(activeSceneWas);
				await SceneManager.UnloadSceneAsync(tempScene);
				await Awaitable.NextFrameAsync();
				GC.WaitForPendingFinalizers();
				GC.Collect();
				GarbageCollector.CollectIncremental(1000000000);
			}
		}
	}

	internal sealed class SerializeReference : MonoBehaviour
	{
		[UnityEngine.SerializeReference] public IArg arg1;
		[UnityEngine.SerializeReference] public IArg arg2;
		[UnityEngine.SerializeReference] public IArg arg3;
		[UnityEngine.SerializeReference] public IArg arg4;
		[UnityEngine.SerializeReference] public IArg arg5;
		[UnityEngine.SerializeReference] public IArg arg6;

		public void Reset()
		{
			arg1 = new IArgImpl1();
			arg2 = new IArgImpl2();
			arg3 = new IArgImpl3();
			arg4 = new IArgImpl4();
			arg5 = new IArgImpl5();
			arg6 = new IArgImpl6();
		}
	}

	public interface IArg { }

	[Serializable] internal class IArgImpl1 : IArg { public bool value; }
	[Serializable] internal class IArgImpl2 : IArg { public bool value; }
	[Serializable] internal class IArgImpl3 : IArg { public bool value; }
	[Serializable] internal class IArgImpl4 : IArg { public bool value; }
	[Serializable] internal class IArgImpl5 : IArg { public bool value; }
	[Serializable] internal class IArgImpl6 : IArg { public bool value; }

	internal sealed class SerializeField : MonoBehaviour
	{
		public Arg arg1;
		public Arg arg2;
		public Arg arg3;
		public Arg arg4;
		public Arg arg5;
		public Arg arg6;
	}

	internal sealed class MonoBehaviourT : MonoBehaviour<Arg1, Arg2, Arg3, Arg4, Arg5, Arg6>
	{
		Arg1 arg1;
		Arg2 arg2;
		Arg3 arg3;
		Arg4 arg4;
		Arg5 arg5;
		Arg6 arg6;

		protected override void Init(Arg1 arg1, Arg2 arg2, Arg3 arg3, Arg4 arg4, Arg5 arg5, Arg6 arg6)
		{
			this.arg1 = arg1;
			this.arg2 = arg2;
			this.arg3 = arg3;
			this.arg4 = arg4;
			this.arg5 = arg5;
			this.arg6 = arg6;
		}
	}

	internal sealed class Singleton : MonoBehaviour
	{
		Arg1 arg1;
		Arg2 arg2;
		Arg3 arg3;
		Arg4 arg4;
		Arg5 arg5;
		Arg6 arg6;

		void Awake()
		{
			arg1 = Arg1.Instance;
			arg2 = Arg2.Instance;
			arg3 = Arg3.Instance;
			arg4 = Arg4.Instance;
			arg5 = Arg5.Instance;
			arg6 = Arg6.Instance;
		}
	}

	internal sealed class Extenject : MonoBehaviour
	{
		[Inject] Arg1 arg1;
		[Inject] Arg2 arg2;
		[Inject] Arg3 arg3;
		[Inject] Arg4 arg4;
		[Inject] Arg5 arg5;
		[Inject] Arg6 arg6;
	}

	internal sealed class NoInjection : MonoBehaviour
	{
		[NonSerialized] public Arg1 arg1;
		[NonSerialized] public Arg2 arg2;
		[NonSerialized] public Arg3 arg3;
		[NonSerialized] public Arg4 arg4;
		[NonSerialized] public Arg5 arg5;
		[NonSerialized] public Arg6 arg6;
	}
}

3 Likes

Why are y’all even comparing the performance of apples and oranges?

[SerializeReference] is pretty straightforwards in it’s implementation. If you have an object marked with the attribute, it’s not serialized inline, but at the bottom of the yaml file, and it gets an identifier that’s used everywhere the same object is referenced.

It is very similar to passing by reference instead of value. Because of this, it will also have somewhat similar performance implications - if you have very many copies of the same object referenced in your MonoBehaviour, it will be much faster to serialize it with [SerializeReference] because the serialization/deserializsation work will be done once. Otherwise it should generally be slower because it has to do more things - both the deserialization work, and the resolution of the reference.

But, like, the performance shouldn’t matter much here because you’re not just willy-nilly throwing [SerializeReference] around for fun. You use it for the two things it provides:

  • the ability to serialize serveral references to the same object once, which allows you to serialize things like graph structures
  • the ability to serialize polymorphic references.

If you need either of those, you have to use [SerializeReference]. If you don’t, you should not use it. So the performance comparison becomes a bit like comparing the fuel effeciency of a submarine and a spaceship.

What would be interesting is a comparison of [SerializeReference] and what we have to do if we’re not using that attribute - ISerializationCallbackReceiver + custom code to achieve the same thing. I suspect that for a bunch of cases, you can tailor the ISerializationCallbackReceiver code to do a smarter thing for your case than the generic SerializeReference behaviour, but I might be wrong there.

2 Likes

I guess its worth noting you can do both of these with Unity objects already. This is what VFX graph does, by just having a bunch of hidden scriptable object sub-objects of the main asset.

Mind you VFX graph came out in Unity 2018, while SerializeReference didn’t come out until Unity 2019, so they didn’t have access to that method of serialization during its inception.

So a comparison of serialization speed between using SerializeReference and regular [SerializeField] references to other Unity objects does make a sense in this context.

Not that I could be buggered to do this test myself. I much prefer not having to deal with a bunch of assets or sub-assets, and simply use [SerializeReference] for both cases as its by far more convenient.

I do use [SerializeReference] an absolute butt-load as well and haven’t noticed any slow downs related to it, either.

1 Like

You’d only ever want to use SerializeReference for a UnityEngine.Object if you’re serializing it through an interface. Which means that there’s still a clean split between the times you use [SerializeReference] and the times that you don’t use it, and that’s got everything to do with functionality and nothing to do with performance.

Not at all what I meant.

Firstly:
A: Unity can’t serialize references to Unity objects via interfaces. Hard limitation of Unity at present.
B: [SerializeReference] on a Unity object type just serializes as if you had used [SerializeField].

Secondly, what I meant is that you can do the same non-linear data structures and polymorphism that you do with [SerializeReference] and non-Unity object types, as you can by using [SerializeField] and Unity object derived types.

3 Likes

My original question was not about what is faster: SerializeReference or SerializeField, but actual overhead of using SerializeReference attribute.

Yes those methods are not interchangeable, but as spiney said before unity added this attribute (and before they made it actually usable and not horribly bugged) people had to deal with serialization problems in various way as polymorphism was not available. For example I am pretty sure there was chunk of people who tried to create huge serializable class with all possible features to offset that problem.

So in this context I think those tests are still valid, because you could use SerializeReference and pay (possibly big) serialization cost, or play with monolithic class with all possible features.

This exists in Unity as well. The default particle system component is like this, and is just shy of 4800 lines of serialized data on its own. This only gets bigger as you play with it, and completely blows out with prefab serialization as well.

A default minimal VXF Graph is less than a 1000 lines of serialized data. And even a reasonably fleshed out VFX graph still comes in at less data than the old particle system.

Though in terms of [SerializeReference] vs sub assets, sub-assets will end up being more serialized data overall, as there’s a bunch of data attached to every sub-object that ends up being more than the assembly qualified type name + data you get with by-reference plain C# objects.

How quickly these two are deserialized compared to one another, again I can’t be bothered to test. But I will say [SerializeReference] should be less memory pressure, as you won’t have a C# object + C++ object for every sub-asset.

But without testing, my intuition tells me [SerializeReference] isn’t that bad compared to the main two alternatives.

1 Like

Hi! I just saw this, 14 days later. I have some info on this and I hope it can still help somebody.
I’ve done a lot of tests for deciding whether to go with ScriptableObject subassets or SerializeReference for some complex data structures. Here are some things I’ve learned:

  • In the Editor, deserializing a single SerializeReference can be like four times faster than cloning a ScriptableObject with Instantiate and deserializing a reference to it (Maybe even a bit more; it’s been a few months since I tested this, so my memory isn’t very fresh).

  • At runtime both things take about the same time.

  • After a SerializeReference is deserialized, other references to the same object are deserialized faster, but it still takes time. A single object referenced in a lot of SerializeReference fields can be a couple of times slower to deserialize, than instantiating a ScriptableObject subasset and deserializing the same number of references to it.

  • When I had some complex graph of objects using SerializeReference to connect them, SerializedObject operations became noticeably slower. Creating the SerializedObject took several seconds, and dragging bound UITK sliders wasn’t smooth at all. I changed several of those objects to ScriptableObject subassets and the UI became a lot more responsive. UITK bindings and Unity serialization is more responsive when a huge Object is broken into multiple UnityEngine.Objects.

  • UnityEngine.Objects definitely use more memory than objects that use SerializeReference, especially in the Editor, but this memory is in the native side. Things like a ScriptableObject’s name, instanceID and all its flags are on the C++ side of the engine, so it won’t affect performance with respect to CPU locality. This is usually by far my main concern with respect to the extra memory, as a game already uses much more RAM on stuff like Textures and 3D models.

Some extra tidbit of info when considering using SerializeReference in MonoBehaviours: There are several important bugs with respect to prefabs that can be very painful and even result in lost work. It’s not overhead, but it can be a good reason to consider other alternatives. For example:

  1. Create a Prefab with a SerializeReference array. Add a bunch of elements to that array.
  2. Instantiate that prefab in a Scene. Reorder the array in the instance and/or override a field inside a managed reference.
  3. Open another Scene.
  4. Delete all the elements in the Prefab Asset’s array.
  5. Open the original Scene again.
  6. Bad things happen. I don’t remember which happen when the order is overridden and which happen when a field is overridden. Sometimes errors are printed, sometimes the Instance’s Component looses all it’s data (even the data outside the array), and sometimes the Instance’s Component can’t be edited anymore.

And there are more bugs related to Prefabs, so I’d say be careful when using SerializeReference in MonoBehaviours.

3 Likes

It’s also good to know that SerializeReference inherently has no multi-editing support.

We’ve ran into quite a bit of trouble caused by bugs and “by design” limitations when using SerializeReference fields in prefabs, to the point of now being quite wary of using them. While it’s a super useful feature, it unfortunately also has a tendency to break in a bunch of edge case scenarios.

1 Like

Only if you don’t have Odin Inspector :u

With respect to prefabs, I’m not sure about yous all, but I tend to find that even a commonly used prefab will have the same data across all or nearly all instances 95% of the time. And all this repeated data gets baked into your scenes.

To that end I prefer to shunt data off to scriptable objects where appropriate. This is a good way to get around the instability when it comes to [SerializeReference] + prefabs.

2 Likes

Oh, that’s a great way to get around that limitation then :ok_hand:

Perhaps they’re using multiple serialized objects behind the scenes to enable this, or just handling undo support and all that manually.