Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

Best method to check something often, without dragging performance

Discussion in 'Scripting' started by DroidifyDevs, Dec 2, 2016.

  1. DroidifyDevs

    DroidifyDevs

    Joined:
    Jun 24, 2015
    Posts:
    1,724
    Hello!

    So as everyone knows, Unity comes with Update, FixedUpdate, LateUpdate functions. Update happens every frame, FixedUpdate happens every physics update, LateUpdate happens after everything in Update is done. However, all of these pre-made functions update very fast. Checking lots of things dozens of times per second slows the game down, especially on mobile.

    For some purposes, I only need to check something a few times a second. To achieve this, I've been using Coroutines, like this:

    Code (CSharp):
    1. IEnumerator CheckRotationStatus()
    2.     {
    3.         //check if object is rotating
    4.         //5 times per second is more than sufficient
    5.         OldRotation = this.transform.rotation;
    6.         yield return new WaitForSeconds(0.2f);
    7.         if (OldRotation == this.transform.rotation)
    8.         {
    9.             NowRotating = false;
    10.         }
    11.  
    12.         else
    13.         {
    14.             NowRotating = true;
    15.             Debug.Log("Old " + OldRotation + " New " + this.transform.rotation);
    16.         }
    17.         StartCoroutine(CheckRotationStatus());
    18.     }
    While I haven't run into performance issues, for my own personal knowledge, is this the best way to check something often without impacting performance much? What other ways can I check something multiple times per second, but much less often than the pre-made Update functions?
     
  2. U7Games

    U7Games

    Joined:
    May 21, 2011
    Posts:
    943
    Right, is a good way, recursive code... cheap and elegant.. i use it too.
     
  3. eisenpony

    eisenpony

    Joined:
    May 8, 2015
    Posts:
    974
    Recursion is great, so long as it ends.

    It seems to me you have an infinitely recursive function here so I bet there will be trouble..

    In this case, using StartCoroutine will return immediately and the caller will exit, so you shouldn't have to worry about a stack overflow. However, calling CheckRotationStatus() is going to create a new Enumerator state machine. Not really a big deal except someone will need to clean that object up once it's run its course. I think this will generate a little bit of garbage that isn't really necessary.
     
  4. DroidifyDevs

    DroidifyDevs

    Joined:
    Jun 24, 2015
    Posts:
    1,724
    Normally I call StartCoroutine(CheckRotationStatus()); on Start. That way it checks 5 times a second forever, without needing to be called again.

    You said "calling CheckRotationStatus() is going to create a new Enumerator state machine". Do you mean that every time I call StartCoroutine(CheckRotationStatus());, I waste more memory on garbage and it doesn't get cleaned up? I thought since it loops it will automatically run the garbage collector.
     
  5. Timelog

    Timelog

    Joined:
    Nov 22, 2014
    Posts:
    528
    Well the GC will run and clean it up eventually. But you have no control over when that will be, so until it comes around and cleans it up it will take up unnecessary memory.

    Is there a reason you don't just have it with a while(true) loop with the WaitForSeconds in it?
     
  6. makeshiftwings

    makeshiftwings

    Joined:
    May 28, 2011
    Posts:
    3,350
    I don't think that's an endless recursion. That StartCoroutine() returns immediately and the function exits, the stack is removed. StartCoroutine() basically just registers the method to be called on the MonoBehaviour. If it was a "yield StartCoroutine()" then it would be an endless recursion, because yield would mean it won't exit until the coroutine finishes.

    Still, like Timelog says, while(true) would probably be cleaner. Also, as to the original question, you can just do it in Update with a timer:

    Code (csharp):
    1. private float nextTime = 0f;
    2.  
    3. void Update() {
    4.   if(Time.time > nextTime) {
    5.     //Check rotation stuff here
    6.     nextTime = Time.time + 0.2f;
    7.   }
    8. }
    This should be slightly faster than the coroutine method, though it probably doesn't matter much.
     
  7. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,513
    Why start a new coroutine if it's just going to run the same code again???

    Code (csharp):
    1.  
    2. IEnumerator CheckRotationStatus()
    3. {
    4.     var wait = new WaitForSeconds(0.2f);
    5.     Quaternion rot;
    6.    
    7.     while(true)
    8.     {
    9.         rot = this.transform.rotation;
    10.         yield return wait;
    11.         //NowRotating = (rot != this.transform.rotation);
    12.         NowRotating = Quaternion.Angle(rot, this.transform.rotation) > Tolerance;
    13.         //where Tolerance is small... like 0.01.
    14.         //due to float error it might be better to use this... your choice
    15.     }
    16. }
    17.  
    Of course other options are the code that starts/stops rotating the object can flag when it's actively doing it, and unflag when it stops. This will be most efficient.

    OR

    If you're dealing with a Rigidbody that is supposed to react to physics as opposed to driven kinematically, you can check the 'IsSleeping' property to know if it's in any sort of motion. The way the physics engine works is it only deals with active rigidbodies that have any physics interacting with it... an object that is not being interacted with gets put into a sleep state until an active force comes in contact with it... at which point it's reincluded into the physics update cycle. So if it's sleeping, it's not moving.

    Here's the thing about this... the physics engines treats this in a sense of linear and rotational speed. And you might want to only consider rotational speed... thing is you're doing a float error prone equality comparison. And a simulation that has forces acting on it is probably going to have SOME degree of both happening to it... if only minor... which your equals test is going to capture.

    So really... 'IsSleeping' is a fairly decent way to determine this status effectively. If only as prone to error as testing every 0.2 seconds.
     
    ZJP, AndyGainey, image28 and 3 others like this.
  8. eisenpony

    eisenpony

    Joined:
    May 8, 2015
    Posts:
    974
    It is recursive because it is defined in terms of itself.
    It is endless because there is no exit criteria.

    I agree that the stack won't overflow because of the implementation but endless recursion is a flag to me that there may be negative side effects and it warrants some investigation. It doesn't mean it's definitively bad, just worth some inspection.

    In this case ...
    Yes
    No

    Like Timelog said, the garbage collector will clear the references up. This has nothing to do with the loop, just the fact that you create an object but don't keep a reference to it. Any time an object exists with no references to it, the garbage collector might clean it. Anyways, my understanding is that reducing garbage is one of the primary micro optimizations for unity games. It does get brought up a lot on these forums..

    I think makeshiftwing's code is good but I like lordofduct's better. It retains the benefits of using a coroutine. The most interesting, to me, is encapsulating the logic.
     
  9. DroidifyDevs

    DroidifyDevs

    Joined:
    Jun 24, 2015
    Posts:
    1,724
    Interesting reading folks!

    I'm making a character move and turn, while he's turning I don't want him to move because that would make him turn in a big circle; so that's why I have that script.

    However I'm not sure if IsSleeping will work, as according to the docs, it applies when BOTH movement and rotation are close to zero. Since I'm just worrying about rotation, it might not be what I need. However it is a cool method that I could use elsewhere.
    I'll test the speed soon by making a few hundred (or thousand) duplicates and see what is the fastest. I'm pretty curious as on a cheap Android device, even those small details count.
     
  10. DroidifyDevs

    DroidifyDevs

    Joined:
    Jun 24, 2015
    Posts:
    1,724
    Here are the results. I copied 8,000 objects into a completely blank scene (no camera, no light). PC specs in signature. FPS from the "stats" button in game view, CPU/RAM data from the Windows Task Manager, V-SYNC off in Unity's Quality settings (to show FPS differences over 60). I had to do 8,000 objects to show the difference, since my PC is quite overpowered compared to my cheap Android tablet (Samsung Galaxy Tab 3 7.0) I'm testing my projects on.

    Code (CSharp):
    1. using UnityEngine;
    2. using System.Collections;
    3. using System.Linq;
    4.  
    5. public class TESTScript : MonoBehaviour {
    6.  
    7.     //My original method
    8.  
    9.     //public int Method1;
    10.     //void Start()
    11.     //{
    12.     //    StartCoroutine(Method1Coroutine());
    13.     //}
    14.  
    15.     //IEnumerator Method1Coroutine()
    16.     //{
    17.     //    yield return new WaitForSeconds(0.2f);
    18.     //    Method1++;
    19.     //    StartCoroutine(Method1Coroutine());
    20.     //}
    21.  
    22.     //RESULTS: 8,000 objects. About 900FPS with strange spikes to 500 every few seconds.
    23.     //In task manager: About 11% CPU, 443.3MB RAM
    24.     //==================================================
    25.  
    26.  
    27.     //private float nextTime = 0f;
    28.     //public int Method2;
    29.  
    30.     //void Update()
    31.     //{
    32.     //    if (Time.time > nextTime)
    33.     //    {
    34.     //        //Check rotation stuff here
    35.     //        Method2++;
    36.     //        nextTime = Time.time + 0.2f;
    37.     //    }
    38.     //}
    39.  
    40.     //RESULTS: 8,000 objects. About 200FPS.
    41.     //In task manager: 16.5% CPU, 501.3MB RAM
    42.     //==================================================
    43.  
    44.  
    45.     //lord of duct reccomended this
    46.     public int Method3;
    47.     void Start()
    48.     {
    49.         StartCoroutine(CheckRotationStatus());
    50.     }
    51.  
    52.     IEnumerator CheckRotationStatus()
    53.     {
    54.         var wait = new WaitForSeconds(0.2f);
    55.         while (true)
    56.         {
    57.             yield return wait;
    58.             //NowRotating = (rot != this.transform.rotation);
    59.             Method3++;
    60.             //where Tolerance is small... like 0.01.
    61.             //due to float error it might be better to use this... your choice
    62.         }
    63.  
    64.     }
    65.  
    66.     //RESULTS: 8,000 objects. FPS randomly between 1500-2000
    67.     //In task manager: 10% CPU, 461MB RAM
    68. }
    69.  
    Pretty unofficial, but apparently lordofduct's method is the best. While my method was lowest on RAM, the FPS suffered massively (for unknown to me reasons). Makeshiftwings's method was the slowest, probably because of the Update function updating 200 times per second.

    Again, there might be a fault with how I ran the experiment, so please let me know if something wasn't right.
     
    Last edited: Dec 3, 2016
    ZJP and eisenpony like this.
  11. U7Games

    U7Games

    Joined:
    May 21, 2011
    Posts:
    943
    never seen a noticeable difference between both methods... but try that fits you best. :)
     
  12. Eric5h5

    Eric5h5

    Volunteer Moderator Moderator

    Joined:
    Jul 19, 2006
    Posts:
    32,401
    The main problem is that you ran the test on a PC; run a test on Android since that's what your actual target is.

    --Eric
     
    eisenpony likes this.
  13. eisenpony

    eisenpony

    Joined:
    May 8, 2015
    Posts:
    974
    I'm honestly surprised by the results for makeshiftwing's approach. I didn't expect it to be much better than lordofduct's but I didn't expect it to be a lot worse either. Maybe there is a penalty for accessing Time.time so often? Or simply for having an OnUpdate method?

    The spikes you saw on your own approach were likely the garbage collector cleaning up the extra enumerables and WaitForSecond objects. You'll notice lordofduct creates a single Wait object before looping to avoid creating another piece of garbage each cycle.
     
    DroidifyDevs likes this.
  14. makeshiftwings

    makeshiftwings

    Joined:
    May 28, 2011
    Posts:
    3,350
    Yeah that's weird. I assumed that a Coroutine's iterator got called every frame the same way Update does, which should mean that they're mostly the same. But maybe WaitForSeconds() does some trick where it doesn't call back from C++ land into C# until after the wait time is over.
     
  15. Eric5h5

    Eric5h5

    Volunteer Moderator Moderator

    Joined:
    Jul 19, 2006
    Posts:
    32,401
    No.

    Yes, Update has a fair amount of overhead.

    --Eric
     
  16. makeshiftwings

    makeshiftwings

    Joined:
    May 28, 2011
    Posts:
    3,350
    I would think the overhead is simply that it is calling from C++ into C#, and thus would be the same as a Coroutine that was just "while(true) yield return null;", right? And the apparent performance increase is that the waiting in WaitForSeconds is done in C++ without calling back into C#? It would be baffling to me if somehow Update() was just generally slower than a Coroutine on a component for no apparent reason.
     
  17. DroidifyDevs

    DroidifyDevs

    Joined:
    Jun 24, 2015
    Posts:
    1,724
    My idea is that because I unlocked V-SYNC, makeshiftwings was checking Time.time every frame. Doing this hundreds of times a second greatly slowed things down. However it was a pretty cool concept :)

    @Eric5h5 has a fair point, so here are results from Android Kit Kat (4.4), Samsung Galaxy Tab 3 7.0. All background apps closed. RAM was cleared in Android's Task manager before each test. System data is from Cool Tool - system stats app. Unfortunately, Cool Tools shows available RAM, not used ram (I couldn't find anything that would show RAM like the Windows task manager). Since it's all relative, it shouldn't be a big deal. Just remember that more RAM available is better.

    Important: There are 100 objects with the script on Android, compared to the 8,000 I ran on Windows 10. I didn't want to overload it too hard, as the CPU with 100 objects was running at almost 80% with little RAM left. Everything else (clear scene, no light, no camera) is the same as in the PC test.

    First, my method:
    Code (CSharp):
    1. public int Method1;
    2.     void Start()
    3.     {
    4.         StartCoroutine(Method1Coroutine());
    5.     }
    6.  
    7.     IEnumerator Method1Coroutine()
    8.     {
    9.         yield return new WaitForSeconds(0.2f);
    10.         Method1++;
    11.         StartCoroutine(Method1Coroutine());
    12.     }
    CPU: 77%
    RAM LEFT: 356MB

    Makeshiftwings's method:
    Code (CSharp):
    1. private float nextTime = 0f;
    2.     public int Method2;
    3.  
    4.     void Update()
    5.     {
    6.         if (Time.time > nextTime)
    7.         {
    8.             //Check rotation stuff here
    9.             Method2++;
    10.             nextTime = Time.time + 0.2f;
    11.         }
    12.     }
    CPU: Fluctuating between 77-85%
    RAM LEFT: 274MB

    lordofduct's method:
    Code (CSharp):
    1. public int Method3;
    2.     void Start()
    3.     {
    4.         StartCoroutine(CheckRotationStatus());
    5.     }
    6.  
    7.     IEnumerator CheckRotationStatus()
    8.     {
    9.         var wait = new WaitForSeconds(0.2f);
    10.         while (true)
    11.         {
    12.             yield return wait;
    13.             //NowRotating = (rot != this.transform.rotation);
    14.             Method3++;
    15.             //where Tolerance is small... like 0.01.
    16.             //due to float error it might be better to use this... your choice
    17.         }
    18.     }
    CPU: 71-77%
    RAM LEFT: 392MB

    I tried to get FPS from FPS Meter app, but all of them showed 51-60. Either they all had the same FPS, or something didn't work right.

    The results are pretty much the same as for Windows 10. It's not scientific, but on 2 different devices on 2 platforms the results were the same.
     
  18. Eric5h5

    Eric5h5

    Volunteer Moderator Moderator

    Joined:
    Jul 19, 2006
    Posts:
    32,401
    For more accurate results it would be a good idea to use the profiler.

    --Eric
     
  19. DroidifyDevs

    DroidifyDevs

    Joined:
    Jun 24, 2015
    Posts:
    1,724
    Oh, true. Only fair if I use the profiler for both Windows 10 and Android 4.4. After some tinkering I got it to connect with USB.

    Here are results from Android Kit Kat (4.4), Samsung Galaxy Tab 3 7.0. All data is from Unity's profiler, connected via USB. Again, 100 objects.

    My method:
    CPU: 25ms-40ms
    Used total RAM: 18.2MB

    Makeshiftwings's method:
    CPU: Very strange results. It would oscilate anywhere between 10ms (100FPS) and 25ms (40FPS)
    Used total RAM: 18MB

    Lordofduct's method:
    CPU: 17ms (59FPS)
    Used total RAM: 18MB

    Slightly different, but still Lordofduct's method stands.
     
  20. image28

    image28

    Joined:
    Jul 17, 2013
    Posts:
    457
    Are they total readings of the project, or just the time the coroutine takes, they seem a little high to be just the coroutine.
     
  21. DroidifyDevs

    DroidifyDevs

    Joined:
    Jun 24, 2015
    Posts:
    1,724
    It's the whole project.
     
  22. GroZZleR

    GroZZleR

    Joined:
    Feb 1, 2015
    Posts:
    3,201
    Measuring for changes like this just seems like bad design in the first place. When the rotation changes, have the changing object notifying any interested parties that the data has changed.

    Imaging having dozens of health bars all periodically sampling to see if an enemy's health has changed before playing some fx and updating the bar. Gross.