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. Voting for the Unity Awards are OPEN! We’re looking to celebrate creators across games, industry, film, and many more categories. Cast your vote now for all categories
    Dismiss Notice
  3. Dismiss Notice

Linking References vs. GetComponent()

Discussion in 'Scripting' started by Vega4Life, Nov 16, 2018.

  1. xVergilx

    xVergilx

    Joined:
    Dec 22, 2014
    Posts:
    3,292
    Yeah, that's what I've pointed out in cons. It's well enough for resolving dependencies. And it's faster. Period.
    Not performing operation is way faster than performing it at all. Serialization engine will probably resolve all references on the native side faster than constantly asking back and forth where the pointer should lead to.

    That applies for defining Awake() messages as well.

    It's managed side reference leak. I don't know how much it actually leaks, but those 8 bytes are only the pointer. I guess hashcode is another 4 bytes (int)? I wonder if anyone could calculate actual leak size to be sure.

    I don't think cleanup should run even as often as 10 seconds, or even hours. Even with 32 bytes leak, its ~30k objects to even exceed 1MB which I doubt ever going to happen faster than one scene passes (for my game for sure). Scene reload is probably better place to do it.

    Clearing up probably will be tricky, unless keys can be simply copied to the buffer, and then purged based on the hashcode from dicts.

    I've just implemented such system, and it is suprisingly works well. Except for the cleanup part, which can be fixed, but it's way over midnight for me to do it right now.


    Scrap that. Completely forgot that Collider is actually a C# wrapper that holds other data too. Now I realized that OnDestroy cleanup is way better in this case, and will probably stick to it. It's executed during scene transitions anyway due to pooling, so no big deal.

    Removing 2 .GetComponent<T> calls per action plus a bunch of components referencing components in exchange of barely noticeable leak is way better in my case. I guess I'll stick to the @Antypodish solution for now.
     
    Last edited: Nov 18, 2018
  2. Suddoha

    Suddoha

    Joined:
    Nov 9, 2013
    Posts:
    2,824
    As I said, work has still to be done either way, no matter which way you go, so "not running at all" does not apply.
    The focus should be put on consistency, and usability from the one's view who's gotta work in the inspector.

    And I really suggest running lots of GetComponent and see what it takes. You might be surprised how well they boosted its performance in the past with their improvements to resolve the component.

    So if you're doing that purely for loading times, be assured it contributes close to nothing compared to what the engine has to do during scene loading. And if loading is slow, there's probably a different bottleneck.

    I'm not saying OnValidate is useless, in fact I like to use it in order to do what the name suggests - validating the fields exposed to the editor (in case I'm too lazy to write a customized editor), which is its primary use and should help whoever messes with the components fields.

    I wouldn't even say the memory it takes is the bad part, it's just the dirty way of doing things and it feels incomplete. A cache should function properly, otherwise it's just a sloppy design, and bad quality.

    After all, with regards to the performance of GetComponent nowadays, it's a fairly unnecessary optimization attempt, since GetComponent is faster than a few major versions ago.
     
  3. xVergilx

    xVergilx

    Joined:
    Dec 22, 2014
    Posts:
    3,292
    It doesn't run in the runtime. Admit it already, there's no faster way than providing serialized reference to the other component. In classic Unity's world.

    From usability perspective - I've really won by using OnValidate. Designers cannot mess up important dependencies, they have a clear and transparent view on whats going on. And most importantly - they don't need to assign a dozens of those dependencies manually, which excludes part of human error. No longer I need to go and tell person - "Yeah, you need to put, this here, this overthere, and this one overhere.". Once they attach the script, it's already done. Combined with some editor extensions I've wrote - I don't even need to write custom editors most of the time, which helps. And it helps a lot. No longer designers need to focus on what to drag where, and me, as programmer is happy not going around and fixing human errors which can be avoided completely.

    If they need to customize the field, it can be done easily as such:
    Code (CSharp):
    1. private void OnValidate () {
    2.      if (Component == null) // Then do stuff
    3. }
    This won't override designers choice, but at the same time it will provide all the needed components, as well as checks if it's assigned at all. It is automated resolution after all.

    OnValidate really makes it faster on mobile. I can tell you that. Having less ops in Awake calls significantly boosts loading time. Serialization provided by native side wins by A LOT. It's all the small things that make difference. No awake calls means less stutter on the first prefab instantiation as well.

    Fun fact. During 2018.3 beta I've lost all my references on the prefabs. Yeah, that shouldn't happened in the first place, but it did. And OnValidate picked up most of the refs correctly, leaving me to only rebuilding UI prefabs that didn't had any OnValidate at all.

    My main point that it is not only makes loading time shorter, but as well provides feedback system to the designers, as well as increases overall robustness of the project.


    TL;DR: I don't think theres anything faster than providing direct ref through serialization, be it inspector, or OnValidate.
    In classic Unity's approach that is. Yeah, UT did a great job optimizing GetComponent, but it's still managed -> native call.

    And I might even say that doing OnValidate is like writing automated tests. If something has failed resolving - you'd know about it. Sure its not 100% required by each and every script. But on the critical parts of the code its well worth having.

    In runtime, customly written lookup is faster. Difference is neglegible, but its there.
    If any complex collider hierarchy is used - then difference become apperent even more.
     
    Last edited: Nov 18, 2018
  4. AndersMalmgren

    AndersMalmgren

    Joined:
    Aug 31, 2014
    Posts:
    5,358
    No version control?
     
  5. xVergilx

    xVergilx

    Joined:
    Dec 22, 2014
    Posts:
    3,292
    There is. I wanted my prefabs to be nested, not to roll back to 2018.2 just because something didn't went right from the start. Roll back didn't helped in my case, so I've rebuilt it UI in a nested way. And it didn't took that much time to be honest.
     
  6. AndersMalmgren

    AndersMalmgren

    Joined:
    Aug 31, 2014
    Posts:
    5,358
    Sounds dangerous, I mean you probably have third party scripts and assets too? I would have resolved it with Unity.
     
  7. xVergilx

    xVergilx

    Joined:
    Dec 22, 2014
    Posts:
    3,292
    Nothing else did completely broke, so it's fine for me. b2->b10 already, really nice and stable.
    They've fixed most of these reference issues in latest betas.
     
  8. AndersMalmgren

    AndersMalmgren

    Joined:
    Aug 31, 2014
    Posts:
    5,358
    I keep out of the betas, the stable releases is beta enough :D
    I needed to use 5.4 beta though because of single pass rendering back then..
     
    xVergilx likes this.
  9. Suddoha

    Suddoha

    Joined:
    Nov 9, 2013
    Posts:
    2,824
    First of all, this is no competition.

    Second of all, fields that are not exposed at all and setup via code cannot be messed up - this applies to Awake as well. In order to achieve the same with OnValidate, you have to serialize a field and make it hidden in the inspector.

    So starting to be picky, you haven't won from any usability point of view - and with regards to actual values that may be constrained to ranges, sets of values or the like - yes, that's what it is very useful for, noone denied that.

    Now once again, you can be as ignorant about it as you want, the deserialization part of the serializazion system has to run at runtime, too. Be it startup, scene startup or whatever. This implies a cost, I've never said it is slower or faster, I simply said it could be either.

    This applies to all programmatic setups, if you don't even let them get their hands on it.
    So this is not even a valid argument against Awake.

    Significant? Just for resolving Awakes and calling them? What are the numbers? I'm curious, because I never seen a slowdown due to Awakes. And I've been really doing some crazy tests for my networking system - on my old mobile as well.

    I also don't notice any slower loading times on the HoloLens which im working with in my job. And believe me, that tends to be a slow device.

    Fun fact: If you can set it up via OnValidate, you can set it up via Awake. Remember, we're talking about GetComponents.

    Once again, if you set things up programmatically, do so. For these parts, you don't need any feedback. Simply don't expose it at all and the amount of feedback reduces just as well.

    Now for the parts that need validation and feedback - that's OnValidates primary use. Once again, noone denied that.
     
  10. Owen-Reynolds

    Owen-Reynolds

    Joined:
    Feb 15, 2012
    Posts:
    1,914
    Suppose you're comfortable having public inspector variables and dragging things into them, like the OP. In that case, OnValidate, to Find and install null inspector entries, is a pretty nice addition.

    It's transparent - it looks and works like public Inspector vars. It's relatively simple - anyone can see OnValidate, look it up, and realize how the trick works. It's incremental - you can set things up as merely Inspector vars; then once you like the script and it seems to need it, you can add OnValidate.
     
  11. xVergilx

    xVergilx

    Joined:
    Dec 22, 2014
    Posts:
    3,292
    I've ran some tests with the following setup, hence startup time testing gives somewhat varying results.
    I've constructed two identical prefabs, and tested their instantiation speed (TestObjectAwake and TestObjectValidate) (Awake is called in this case, also it does affect instantiation speed as well, which is way more important than loading objects in the scene).

    Each test is ran a couple of times (plus additional three times per start), just to avoid some errors, on the i3-2310 (2.10 Ghz) (Laptop).

    For the first test I've spawned 1000 of TestObjectAwakes, and got following results (avg) in ms:
    Around 69 ms for 1000 objects spawned

    Following results are from TestObjectValidateAwake, that has same conditions, and for the sake of fairness, .Awake() method declared as well. Got following results:

    Around 67 ms for 1000 objects spawned (-2ms with Awake call)

    Around 65 ms for 1000 objects spawned (w/o Awake call). (-5ms won)

    Sure, this isn't mobile, neither close to the real world example as not always it's possible not to define .Awakes.
    Bear in mind, GetComponent is actually really fast, main issue with it is that its an extern call.

    But, there's always a but. Usually people don't use only GetComponent calls, but GetComponentInChildren, GetComponentInParent, as well as Finds.

    I'll run a couple of tests with those as well, once I've got some spare time. In the meantime, I suggest grabbing following sources and trying out yourself. On mobile as well. There might be bigger / lesser difference.

    Test.cs:
    Code (CSharp):
    1. using System.Diagnostics;
    2. using UnityEngine;
    3. using Debug = UnityEngine.Debug;
    4.  
    5. public class Test : MonoBehaviour {
    6.    public GameObject TestPrefab;
    7.    public int Iterations = 1000;
    8.  
    9.    private void Start() {
    10.       Stopwatch stopWatch = new Stopwatch();
    11.  
    12.       stopWatch.Start();
    13.  
    14.       for (int i = 0; i < Iterations; i++) {
    15.          GameObject testObj = Instantiate(TestPrefab);
    16.          Destroy(testObj);
    17.       }
    18.  
    19.       stopWatch.Stop();
    20.  
    21.       Debug.Log("Total time: " + stopWatch.Elapsed.TotalMilliseconds);
    22.  
    23.       stopWatch.Reset();
    24.       stopWatch.Start();
    25.  
    26.       for (int i = 0; i < Iterations; i++) {
    27.          GameObject testObj = Instantiate(TestPrefab);
    28.          Destroy(testObj);
    29.       }
    30.  
    31.       stopWatch.Stop();
    32.  
    33.       Debug.Log("Total time: " + stopWatch.Elapsed.TotalMilliseconds);
    34.  
    35.       stopWatch.Reset();
    36.       stopWatch.Start();
    37.  
    38.       for (int i = 0; i < Iterations; i++) {
    39.          GameObject testObj = Instantiate(TestPrefab);
    40.          Destroy(testObj);
    41.       }
    42.  
    43.       stopWatch.Stop();
    44.  
    45.       Debug.Log("Total time: " + stopWatch.Elapsed.TotalMilliseconds);
    46.    }
    47. }
    TestObjectAwake.cs:

    Code (CSharp):
    1. using UnityEngine;
    2.  
    3. public class TestObjectAwake : MonoBehaviour {
    4.     private TestComponent _ref;
    5.  
    6.     private void Awake() {
    7.         _ref = GetComponent<TestComponent>();
    8.     }
    9. }
    10.  
    TestObjectValidateAwake.cs:

    Code (CSharp):
    1. using UnityEngine;
    2.  
    3. public class TestObjectValidateAwake : MonoBehaviour {
    4.    [SerializeField]
    5.    private TestComponent _ref;
    6.  
    7.    private void Awake() { }
    8.  
    9.    private void OnValidate() { _ref = GetComponent<TestComponent>(); }
    10. }
    11.  
    TestObjectValidate.cs:
    Code (CSharp):
    1. using UnityEngine;
    2.  
    3. public class TestObjectValidate : MonoBehaviour {
    4.    [SerializeField]
    5.    private TestComponent _ref;
    6.  
    7.    private void OnValidate() { _ref = GetComponent<TestComponent>(); }
    8. }
    9.  

    Also,
    I was pointing towards inspector assigned variables, not GetComponents in this case. But you're right.

    Unfortunately, not all references can be assigned via .Awake / .OnValidate due to design decisions.
     
    Last edited: Nov 19, 2018
  12. AndersMalmgren

    AndersMalmgren

    Joined:
    Aug 31, 2014
    Posts:
    5,358
    That i3 is darn close to mobile performance though :p
     
    xVergilx likes this.
  13. xVergilx

    xVergilx

    Joined:
    Dec 22, 2014
    Posts:
    3,292
    Nah, still runs better than most mobiles.

    Who am I kidding, you're right. Lmao. But, can you run Unity Editor on mobile?
     
    AndersMalmgren likes this.
  14. xVergilx

    xVergilx

    Joined:
    Dec 22, 2014
    Posts:
    3,292
    So, I've went completely lazy this time. Less test runs (5 / Source), more components, Awake declared in both case scenarios.

    This time I've added 4 extra components, in both cases retrieved by GetComponent<T>.
    (TestComponent as an extra empty script, rigidbody, collider, audio source and particle system).
    Identical setup with both prefabs, with an exception of the scripts running by themselves.

    Also, decreased object count to 500 as opposing to 1000, because my poor laptop / Unity didn't handled very well.
    Results:
    upload_2018-11-20_20-50-6.png

    Almost same picture, about 1ms / per call, OnValidate wins again with about 4-5 ms (important to note that this is with Awake declared as well!). On 500 objects that would be 10ms won I'd say, as time seems to be increasing linearly.
    upload_2018-11-20_20-52-25.png upload_2018-11-20_20-52-46.png
    Code (CSharp):
    1. using UnityEngine;
    2.  
    3. public class TestObjectAwake : MonoBehaviour {
    4.     private TestComponent _ref;
    5.     private Rigidbody _rigidbody;
    6.     private Collider _collider;
    7.     private AudioSource _audioSource;
    8.     private ParticleSystem _ps;
    9.  
    10.     private void Awake() {
    11.         _ref = GetComponent<TestComponent>();
    12.         _rigidbody = GetComponent<Rigidbody>();
    13.         _collider = GetComponent<Collider>();
    14.         _audioSource = GetComponent<AudioSource>();
    15.         _ps = GetComponent<ParticleSystem>();
    16.     }
    17. }
    18.  
    Code (CSharp):
    1. using UnityEngine;
    2.  
    3. public class TestObjectValidateAwake : MonoBehaviour {
    4.    [SerializeField]
    5.    private TestComponent _ref;
    6.  
    7.    [SerializeField]
    8.    private Rigidbody _rigidbody;
    9.  
    10.    [SerializeField]
    11.    private Collider _collider;
    12.  
    13.    [SerializeField]
    14.    private AudioSource _audioSource;
    15.  
    16.    [SerializeField]
    17.    private ParticleSystem _ps;
    18.  
    19.    private void Awake() { }
    20.  
    21.    private void OnValidate() {
    22.       _ref = GetComponent<TestComponent>();
    23.       _rigidbody = GetComponent<Rigidbody>();
    24.       _collider = GetComponent<Collider>();
    25.       _audioSource = GetComponent<AudioSource>();
    26.       _ps = GetComponent<ParticleSystem>();
    27.    }
    28. }
    29.  

    Next up - x5 GetComponentInChildren<T>(true), with full child separated hierarchy. Same components, different placement as children of the root object with the script attached to it (ran into 5 image attachement limit here, so no screenshot). 1 object depth as it is.
    Results:
    upload_2018-11-20_21-9-3.png
    Still, about 5 ms won. Not much I guess UT done well optimizing their GetComponentChildren<T> method. Still fast, good enough.

    10ms for 1k objects that will be.
    Not that of a complex hierarchy, but I don't think anyone sane enough will build their prefabs 5+ objects deep. (Unless doing some weird stuff, or rigging). Plus, I'm lazy to build it, so, deal with it,

    Code (CSharp):
    1. using UnityEngine;
    2.  
    3. public class TestObjectAwake : MonoBehaviour {
    4.     private TestComponent _ref;
    5.     private Rigidbody _rigidbody;
    6.     private Collider _collider;
    7.     private AudioSource _audioSource;
    8.     private ParticleSystem _ps;
    9.  
    10.     private void Awake() {
    11.         _ref = GetComponentInChildren<TestComponent>(true);
    12.         _rigidbody = GetComponentInChildren<Rigidbody>(true);
    13.         _collider = GetComponentInChildren<Collider>(true);
    14.         _audioSource = GetComponentInChildren<AudioSource>(true);
    15.         _ps = GetComponentInChildren<ParticleSystem>(true);
    16.     }
    17. }
    18.  
    Code (CSharp):
    1. using UnityEngine;
    2.  
    3. public class TestObjectValidateAwake : MonoBehaviour {
    4.    [SerializeField]
    5.    private TestComponent _ref;
    6.  
    7.    [SerializeField]
    8.    private Rigidbody _rigidbody;
    9.  
    10.    [SerializeField]
    11.    private Collider _collider;
    12.  
    13.    [SerializeField]
    14.    private AudioSource _audioSource;
    15.  
    16.    [SerializeField]
    17.    private ParticleSystem _ps;
    18.  
    19.    private void Awake() { }
    20.  
    21.    private void OnValidate() {
    22.       _ref = GetComponentInChildren<TestComponent>(true);
    23.       _rigidbody = GetComponentInChildren<Rigidbody>(true);
    24.       _collider = GetComponentInChildren<Collider>(true);
    25.       _audioSource = GetComponentInChildren<AudioSource>(true);
    26.       _ps = GetComponentInChildren<ParticleSystem>(true);
    27.    }
    28. }
    29.  

    TL;DR:
    Does that mean it's not worth it? Probably. GetComponent<T> / GetComponentChildren<T> placement is irrelevant on PC. Probably numbers would be x2-x4 times higher on mobile.
    Is 40 ms win worth moving some code to different method? In my opinion - every small boost count.

    I'd still stick to the OnValidate though. Complex pre-calculations, branches, and sanity checks are better off performed there, rather than glued together with debug defines in the Awake.

    Also, Finds are obviosly slow, so I don't feel like its going to be a revelation to anyone. Won't be running any of those. GetComponentInParent()? Not that useful in the current world, and too specific. Maybe on weekend, if anyone else will be interested in the results.
     
    Last edited: Nov 20, 2018
    Suddoha likes this.
  15. Suddoha

    Suddoha

    Joined:
    Nov 9, 2013
    Posts:
    2,824
    Thanks for the tests.
    I'm not extremly surprised though, at least by knowing these results were produced on an average PC (as your description suggests).

    One more thing that came to my mind when I was at work today and saw my code: I love supplying interfaces for extensibility purposes (a single base class to build onto is often too restrictive) and that's another reason why I got used to set things up programmatically in Awake (whenever I can). OnValidate could be used to tell someone an ISomething is required and that's a perfectly valid and great way to use it, but I wouldn't be able to serialize an arbitary ISomething as an actual component type without any workaround or a strict limitation to a single base class.

    Of course every opzimization that yields improvements is worth it as long as it doesn't force you to do give up on other benefits that you may need.

    Personally, I cannot and/or don't want to lose the flexibility I mentioned above. Knowing that I potentially only loose a very tiny fraction of loading time (~4ms to 5ms at a total time of ~220ms) that's absolutely fine.
    I mean, suppose it really scales proportionally, a loading time 100 times as long would only lead to roughly 400 extra millseconds. Whether a super-large game loads 22000ms (22 seconds) or 22,4 seconds...

    For all the other things that are meant to be messed with in the inspector: OnValidate is perfectly fine, I definitely agree.