Even More Ways Structs Create Garbage
Last time we saw that calling a non-default constructor on a generic struct (MyStruct<T>
) causes garbage creation. That garbage creation is subtle, but can have big impacts on framerate and memory usage. Today we’ll see two more ways that structs can create garbage and hopefully avoid some pitfalls. Read on to find out how!
To recap, here’s the test from last time:
using UnityEngine; public class TestScript : MonoBehaviour { struct NormalStruct { public int Val; public NormalStruct(int val) { Val = val; } } struct GenericStruct<T> { public T Val; public GenericStruct(T val) { Val = val; } } void Start() { TestNormalStructDefaultCtor(); TestNormalStructParamsCtor(); TestGenericStructDefaultCtor(); TestGenericStructParamsCtor(); } void TestNormalStructDefaultCtor() { var s = new NormalStruct(); s.Val = 0; } void TestNormalStructParamsCtor() { new NormalStruct(0); } void TestGenericStructDefaultCtor() { var s = new GenericStruct<int>(); s.Val = 0; } void TestGenericStructParamsCtor() { new GenericStruct<int>(0); } }
None of the tests created any garbage except the one that called the constructor with parameters on a generic structure: new GenericStruct
. This made me wonder what else would cause garbage creation with a generic structure, so I added some more test cases:
- Get a field
- Set a field
- Get a property
- Set a property
- Call a function that returns a field
- Call a function that sets a field
Which of these will create garbage? Will any of them create garbage when used with a non-generic structure? To find out, I ran this test script in Unity 5.3.4f1 with the profiler set to “deep” mode:
using UnityEngine; public class TestScript : MonoBehaviour { struct NormalStruct { public int Val; public NormalStruct(int val) { Val = val; } public int ValProp { get { return Val; } set { Val = value; } } public int GetVal() { return Val; } public void SetVal(int val) { Val = val; } } struct GenericStruct<T> { public T Val; public GenericStruct(T val) { Val = val; } public T ValProp { get { return Val; } set { Val = value; } } public T GetVal() { return Val; } public void SetVal(T val) { Val = val; } } void Start() { TestNormalStructDefaultCtor(); TestNormalStructParamsCtor(); TestNormalStructGetField(); TestNormalStructSetField(); TestNormalStructGetProp(); TestNormalStructSetProp(); TestNormalStructGetFunc(); TestNormalStructSetFunc(); TestGenericStructDefaultCtor(); TestGenericStructParamsCtor(); TestGenericStructGetField(); TestGenericStructSetField(); TestGenericStructGetProp(); TestGenericStructSetProp(); TestGenericStructGetFunc(); TestGenericStructSetFunc(); TestGenericStructMultiple(); } void TestNormalStructDefaultCtor() { var s = new NormalStruct(); s.Val = 0; } void TestNormalStructParamsCtor() { new NormalStruct(0); } int TestNormalStructGetField() { var s = new NormalStruct(); return s.Val; } void TestNormalStructSetField() { var s = new NormalStruct(); s.Val = 0; } void TestNormalStructGetProp() { var s = new NormalStruct(); var v = s.ValProp; } void TestNormalStructSetProp() { var s = new NormalStruct(); s.ValProp = 0; } int TestNormalStructGetFunc() { var s = new NormalStruct(); return s.GetVal(); } void TestNormalStructSetFunc() { var s = new NormalStruct(); s.SetVal(0); } void TestGenericStructDefaultCtor() { var s = new GenericStruct<int>(); s.Val = 0; } void TestGenericStructParamsCtor() { new GenericStruct<int>(0); } int TestGenericStructGetField() { var s = new GenericStruct<int>(); return s.Val; } void TestGenericStructSetField() { var s = new GenericStruct<int>(); s.Val = 0; } void TestGenericStructGetProp() { var s = new GenericStruct<int>(); var v = s.ValProp; } void TestGenericStructSetProp() { var s = new GenericStruct<int>(); s.ValProp = 0; } int TestGenericStructGetFunc() { var s = new GenericStruct<int>(); return s.GetVal(); } void TestGenericStructSetFunc() { var s = new GenericStruct<int>(); s.SetVal(0); } int TestGenericStructMultiple() { var s = new GenericStruct<int>(0); var v = s.ValProp; s.ValProp = 0; v = s.GetVal(); s.SetVal(0); return v; } }
On the initial run, only TestGenericStructParamsCtor
created any garbage: 32 bytes. However, I commented out that test and suddenly another test created 32 bytes of garbage: TestGenericStructGetProp
. I don’t know why it didn’t create any garbage on the first run when TestGenericStructParamsCtor
was enabled, but if you do drop me a line in the comments.
I kept commenting out tests that created garbage until none were left that created any. In all, these tests each created 32 bytes of garbage:
TestGenericStructParamsCtor
TestGenericStructGetProp
TestGenericStructSetProp
TestGenericStructGetFunc
TestGenericStructSetFunc
TestGenericStructMultiple
Notice that TestGenericStructMultiple
is a conglomeration of all the other tests that create garbage. It shows that the amount created is for the struct
itself, not the function calls being made on it. It’s simply the usage of the functions that necessitate garbage creation in the first place.
The takeaway here is that any function, non-default constructor, or property on a generic struct
will cause that struct
to be created as garbage when you use new
. You’re free to read or write the fields directly or use the default (no parameters) constructor without creating any garbage, even when the struct
is generic. Without using generics, you’re free to use any of these features without fear of garbage creation.
If you’ve got any insight into why garbage is sometimes created with generic structures, let me know in the comments. Or feel free to share your experiences with garbage creation and structs.
#1 by Hjalmar Wallander on June 9th, 2016 ·
What happens if you put all of these tests in an ‘Update’ instead? Do they create 32 bytes on each run, or is it just the first time?
#2 by jackson on June 9th, 2016 ·
If you just renamed the
Start
function toUpdate
then you’d get the full 192 bytes (6 * 32) of garbage created every frame the script ran. That would add up quickly! The GC would run pretty frequently and constantly create frame spikes in your game, not to mention the memory fragmentation.#3 by ms on February 23rd, 2017 ·
great info, thanks
#4 by Rushburger on June 27th, 2019 ·
Great article! I find this from your another article : https://jacksondunstan.com/articles/3471
In this article , you said that
” The next tradeoff being made by IEnumerable is right in its name: it’s an interface. The interface automatically means that each function is virtual and can’t be implemented by a generic struct (i.e. MyStruct) without the struct needing to be allocated on the heap as garbage for the GC to later collect.”
And I was wandering how a generic struct implementing an interface can cause GC?
#5 by jackson on June 27th, 2019 ·
I’m glad you liked the article. :)
If you follow the link at the end of that quote, you’ll see an article showing several examples of how generic structs resulted in garbage creation while non-generic structs did not. That said, the article is a few years old and may not apply to newer Unity versions and definitely not to the Burst compiler as the code it compiles will never generate garbage but does support generics.
#6 by Rushburger on June 27th, 2019 ·
Okay, I see the point. Thanks!