Get Component vs Actions: A Deep Dive
This whole adventure started out when I was reviewing a few students code, and saw how they were calling functions on other objects during an on trigger enter call. On trigger enter, they sent out an event passing in the other collider from the trigger, and then each subscriber compared that game object to themselves. If they comparison came back true, then we had a match and we call whatever function was added to that delegate. Looking at it, it didn't make much sense to me, we already know who the other game object is through the trigger, why make all these extra calls when we can just get the script component off of them and call it a day. I decided to do some investigating as the Action method was being taught as the more optimal way. A quick run through the profiler, and I found that at first glance Actions were in fact more optimized. I was getting 6kb of garbage collection on get component, where I was getting 0 on calling the event. Case closed right? Wrong. I noticed I never actually attached the component they were looking for, so just to be sure I attached it and gave it a run. All the sudden the 6kb of garbage collection dropped to 0. So I did some research and found that what is causing that garbage collection is not the get component itself but actually comes from a separate warning string. Rather than me trying to sum it up I’ll show it to you straight from the source. This excerpt comes from the Unity Blog in a post by Lucas Meijer “We do this in the editor only. This is why when you call GetComponent() to query for a component that doesn’t exist, that you see a C# memory allocation happening, because we are generating this custom warning string inside the newly allocated fake null object. This memory allocation does not happen in built games. This is a very good example why if you are profiling your game, you should always profile the actual standalone player or mobile player, and not profile the editor, since we do a lot of extra security / safety / usage checks in the editor to make your life easier, at the expense of some performance. When profiling for performance and memory allocations, never profile the editor, always profile the built game.” According to Lucas this was only happening in the editor, so I built out my project, slapped a profiler on it and sure enough no garbage collection on the get component calls. This is when I stumbled upon TryGetComponent. Try get component skips the null check and gives you and Out Variable much like a Raycast. We can check it with an if statement and it wont collect garbage in the editor. In use it looks like the following:
I really like this as it is much cleaner looking then your traditional null check. So now we have 3 different methods of getting an objects component. Lets put them against each other and see which one comes out on top. This was done in the actually build so no garbage allocation occurs. Here is the data
Lets break down what we are looking at. We have 4 different tests in here, Actions, Try Get Component — Null and Not Null results, Get Component with a null check — Null and Not Null Results, and lastly a Get Component that doesn't null check. We wont ever really use the last one but I thought it would be worth a look just incase something weird was happening. While the times are all below .00ms we can still visualize the differences between them based off of the percent of the update loop in terms of time. These are actually currently ranked, but there's no easy way to show you the percent's without 8 more screenshots so I will just list them out
- NoNullCheck GetComponent — is null: 1.85% — you will never use this unless you are caching in start . Even then a null check is a good idea.
- TryGetComponent — is null: 2.78%
- NoNullCheck GetComponent — Not null : 2.78 — same note as above
- GetComponent — is null: 4.63%
- TryGetComponent — Not Null: 5.56%
- ActionInvoke — 8.33%
- Action — this is the action getting called, we can’t measure it by percent as they are located in different scripts, we know it is slower due to its positioning though.
- GetComponent — NullCheck — Not Null: 51%
So what can we make of this data? Well we see that TryGetComponent is actually the fastest, where GetComponent is the slowest. We also see that when the component is null, we get faster speeds, due to it not actually having to call a function. This is the most optimal situation for the Action as there is only one object subscribed. Lets take a look at 10, 100, and 1000 objects.
At 10 subscribers our Action Invoke jumps up to 30%, we are not below GetComponent yet, but not even close to TryGetComponent who is still sitting around 2%.
At 100 subscribers our Action Invoke is now sitting at the bottom of the leader board. Lower than Get Componenet not null. Our invoke is eating up 68% and we are doing 100 calls on the action. We are officially on the time board at .07 ms.
At 1000 subscribers we are sitting at 97%. Our time in ms is .5 for the invoke, and .19 for the action itself, which is getting called 1000 times. TryGetComponent, sits comfortably at the top with .08% of time spent. Even our regular get component is looking pretty nice here.
So what does this mean for you in your game? Probably nothing. Were talking about thousandths of milliseconds here. But it does mean that you can keep using get component, so long as you switch to TryGetComponent and you wont be losing performance against an action. Now 1000 objects is extreme, there are very few scenarios where you are going to run into having this many subscribers. But 100? Not as far fetched. For our case for this project, a tower defense came, it is not unheard of to have a huge amount of enemies on screen at once. So if you do decide to go with actions here just keep in mind it has diminishing return on amount of subscribers. So what was the reason for going down this rabbit hole? Stubbornness mainly. I've always used get component on collision events and I don't like change. Shooting out an action after we already have the game object seemed like an extra step that can get messy, especially if you tend to get disorganized easily. But the take away is that the original theory was correct, Actions are faster than Get Component. But try get component seems to take the cake on optimization, between these options. Call that 1000 times and congratulations, you just saved a millisecond.