Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Dismiss Notice

Question GetComponent does not allocate?

Discussion in 'Scripting' started by DevDunk, Aug 9, 2023.

  1. DevDunk

    DevDunk

    Joined:
    Feb 13, 2020
    Posts:
    4,362
    I often get the VS comment to change the following, since GetComponent (while null) can generate garbage

    Code (CSharp):
    1. var comp = GetComponent<Transform>();
    2.             if (comp != null) ReallyImportantCode.DoSomething<Transform>(comp);
    into
    Code (CSharp):
    1. if (TryGetComponent<Transform>(out var comp))
    2.                 ReallyImportantCode.DoSomething<Transform>(comp);
    When benchmarking the code with the code below, I get that GetComponent with the component being null has terrible editor performance, but fine build performance, with no garbage allocated.

    The implementation is very naive, but I wondered if this is something that has changed over the years.

    Testing code:
    Code (CSharp):
    1.  Profiler.BeginSample("GetComponent True");
    2.         for (int i = 0; i < RepetitionsPerFrame; i++)
    3.         {
    4.             var comp = GetComponent<Transform>();
    5.             if (comp != null) ReallyImportantCode.DoSomething<Transform>(comp);
    6.         }
    7.         Profiler.EndSample();
    8.  
    9.         Profiler.BeginSample("TryGetComponent True");
    10.         for (int i = 0; i < RepetitionsPerFrame; i++)
    11.         {
    12.             if (TryGetComponent<Transform>(out var comp))
    13.                 ReallyImportantCode.DoSomething<Transform>(comp);
    14.         }
    15.         Profiler.EndSample();
    16.  
    17.         Profiler.BeginSample("GetComponent False");
    18.         for (int i = 0; i < RepetitionsPerFrame; i++)
    19.         {
    20.             var comp = GetComponent<Animator>();
    21.  
    22.             if (comp != null) ReallyImportantCode.DoSomething<Animator>(comp);
    23.         }
    24.         Profiler.EndSample();
    25.  
    26.         Profiler.BeginSample("TryGetComponent False");
    27.         for (int i = 0; i < RepetitionsPerFrame; i++)
    28.         {
    29.             if (TryGetComponent<Animator>(out var comp)) ReallyImportantCode.DoSomething<Animator>(comp);
    30.         }
    31.         Profiler.EndSample();
    upload_2023-8-9_22-6-33.png upload_2023-8-9_22-6-43.png
     
  2. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    5,769
    GetComponent
    in editor (but not build) allocates a fake Unity null object (for debug purpose).
    TryGetComponent
    does not.
     
    DevDunk and Bunny83 like this.
  3. MartinTilo

    MartinTilo

    Unity Technologies

    Joined:
    Aug 16, 2017
    Posts:
    2,140
    Correct, and I can't remember if this was ever different.

    What I find interesting from your benchmark and what should be the reason to use TryGetComponent for you, is that both failing and succeeding the getting the component it is more performant than successfully getting it with GetComponent. So unless you expect to nearly always fail getting it, it would be the more performant call on average.

    Also, unless your checking for a MonoBehaviour or if that component is assigned to a MB field in the inspector, the first time any script accesses a component this way the Managed Shell for that object WILL be allocated then. It'll be retained for (at least) the lifetime of the component and reused on every subsequent access, so it's not directly "garbage" but that allocation will have some performance cost.

    Oh and something that DID change: those
    EditorOnly [<marker name>]
    entries used to be just GC.Alloc samples under the GetComponent call that would add their GC Alloc amount to the parent scopes. To help guide optimization efforts towards what actually matters for performance in a build, we introduced these EditorOnly markers that "take the blame" for the GC Alloc and do not propagate it up the chain. Otherwise that
    GetComponent False
    marker and it's parents would be shown to inherit nearly 6MB in the GC Alloc column.

    The EditorOnly markers can't "consume" the time taken for them in the same manner though as that would mess with the totals.
     
    _geo__, Lurking-Ninja, Ryiah and 3 others like this.
  4. MartinTilo

    MartinTilo

    Unity Technologies

    Joined:
    Aug 16, 2017
    Posts:
    2,140
    On the flip side, that GC Alloc hiding + GC Alloc happening across all threads sometimes leads to confusion as the Main Threads totals for GC Alloc and the Memory Profiler GC Allocated In Framed totals across all threads not matching up is it's own source of confusion... I guess you can't win on all fronts...
     
    DevDunk and spiney199 like this.
  5. wideeyenow_unity

    wideeyenow_unity

    Joined:
    Oct 7, 2020
    Posts:
    728
    If I'm not mistaken, any temp variable declared like this, will create garbage. Since "var comp" is not cached, or saved in any way.

    But how much garbage to collect, or how un-performant it exactly is? I'm not sure. I use temp variables quite regularly, so I can't agree that it's a major issue.
     
    DevDunk likes this.
  6. DevDunk

    DevDunk

    Joined:
    Feb 13, 2020
    Posts:
    4,362
    It's definitely not a major issue if you don't do it everywhere.
    Was running some comparison in benchmarking and found the difference between build and editor interesting
     
  7. Saniell

    Saniell

    Joined:
    Oct 24, 2015
    Posts:
    164
    That's called local variable and they are stored on stack and do not GCAlloc anything
     
    Bunny83 likes this.
  8. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    5,769
    Assigning a reference to a local variable doesn't inherently allocate. In this case if you're getting a reference to something already present, there's no allocation. If you're, say, instancing a class, then you will allocate something.

    As Martin said though, if you're accessing a Unity object for the first time it will allocate a managed shell, but not on subsequent accesses.
     
    MartinTilo likes this.
  9. Baste

    Baste

    Joined:
    Jan 24, 2013
    Posts:
    6,186
    The tooltip in Visual Studio is bad here. I noticed it immediately when I read the blog post by @jbevain-msft - the second screenshot in the article reads "GetComponent allocates even if no component is found", which is an editor-only issue, so it's slightly wrong. Somebody should probably update the relevant package :p

    That's a super interesting link. After all these years, I had not realized that the C# wrapper for builtin types was created on-demand rather than when the native object was loaded. It totally makes sense as an optimization, so I'm not very surprised, but for some reason I just assumed it was the other way around. You learn something new every day!

    This does mean that this could influence the result of @DevDunk's test - since GetComponent is tested before TryGetComponent, it's the first GetComponent call that generates the managed shell, so the cost of that goes into the GetComponent profiler scope. So if GetComponent and TryGetComponent were switched, you'd get a bit more time in TryGetComponent and a bit less in GetComponent. To get the correct results, you should probably call GetComponent<Transform> outside of the profiler scope one time in order to remove that cost.

    I hadn't seen this yet (haven't been profiling in editor in newer versions of Unity), but it's really cool! That should help people a ton with understanding what's going on, so thanks for introducing that.
     
    MartinTilo and Bunny83 like this.
  10. CodeRonnie

    CodeRonnie

    Joined:
    Oct 2, 2015
    Posts:
    280
    I also noticed this recently when doing some performance tests. It seemed to me that TryGetComponent was strictly better in terms of performance, which is nice because I like methods with the bool TrySomething<T>(out T) syntax.
     
  11. DevDunk

    DevDunk

    Joined:
    Feb 13, 2020
    Posts:
    4,362
    Interesting.
    Which Unity version was this on?
    And was this in a build? Mono or IL2CPP? (mine was IL2CPP Unity 2022.3.6)
     
  12. wideeyenow_unity

    wideeyenow_unity

    Joined:
    Oct 7, 2020
    Posts:
    728
    If I'm not mistaken, the "TryOut" method creates no garbage. So if handling tons of these in a frame, garbage collection would surely hinder performance. Although I'm not 100% sure if it creates no garbage, just thinking aloud.
     
  13. DevDunk

    DevDunk

    Joined:
    Feb 13, 2020
    Posts:
    4,362
    In builds GetComponent also generates no immediate garbage it seems. Just in editor.
    So it mostly is a readability thing (?)
    I always used TryGetComponent if I use null checks, like the analyzer suggests
     
  14. wideeyenow_unity

    wideeyenow_unity

    Joined:
    Oct 7, 2020
    Posts:
    728
    I mean if you do:
    Code (CSharp):
    1. var script = GetComponent<Script>();
    2. script.functionA(true);
    3. script.speed += 5f;
    Does not making the script a temp variable that way, create garbage? I have heard conflicting reports, so I just assume it always does.
     
  15. DevDunk

    DevDunk

    Joined:
    Feb 13, 2020
    Posts:
    4,362
    I think it is passed by reference, not value.
    The only difference with TryGetComponent would be that when the value is null it would not continue to run the code, but when the value is not null it should do the same, since it's not manually managing memory like with Using
     
  16. CodeRonnie

    CodeRonnie

    Joined:
    Oct 2, 2015
    Posts:
    280
    When I was testing it was only in editor. I'll do some more testing today, and make sure to do it in a build.
     
    DevDunk likes this.
  17. wideeyenow_unity

    wideeyenow_unity

    Joined:
    Oct 7, 2020
    Posts:
    728
    That's a good point. But if temp-ing a script with GetComponent() is garbage free, that will give me more flexibility in future projects. thanks for the info. :)
     
    DevDunk likes this.
  18. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    5,769
    TryGetComponent and GetComponent basically do the same thing under the hood, just the former ends in a Unity object null check. From the source code.

    GetComponent<T>

    Code (CSharp):
    1. [System.Security.SecuritySafeCritical]
    2. public unsafe T GetComponent<T>()
    3. {
    4.     var h = new CastHelper<T>();
    5.     GetComponentFastPath(typeof(T), new System.IntPtr(&h.onePointerFurtherThanT));
    6.     return h.t;
    7. }
    TryGetComponent<T>(out T)

    Code (CSharp):
    1. [System.Security.SecuritySafeCritical]
    2. public unsafe bool TryGetComponent<T>(out T component)
    3. {
    4.     var h = new CastHelper<T>();
    5.     TryGetComponentFastPath(typeof(T), new System.IntPtr(&h.onePointerFurtherThanT));
    6.     component = h.t;
    7.     return h.t != null;
    8. }
    So all in all, GetComponent<T> + null check, compared to TryGetComponent, should probably have the same performance impact, editor allocation aside.