Coroutines are badass

Discussion in 'iOS Development' started by n0mad, Aug 22, 2009.

  1. n0mad

    n0mad

    Member

    Joined:
    Jan 27, 2009
    Messages:
    3,731
    Just wanted to emphasize that well-established fact :

    Coroutines are awesome for performance optimization :)

    I turned to a coroutine an old Update() function that just checked if a fixed float was superior to Time.time, in which case it would turn a boolean to true. And it made me gain 5 fps.

    Yeah, that much. The coroutine removed that checkout by randomly changing the boolean every 1 to 4 seconds.

    Now I can imagine how my actual fps would benefit from turning every looped methods into coroutines ^^
  2. Jessy

    Jessy

    Member

    Joined:
    Jun 7, 2007
    Messages:
    7,173
    If you're doing anything where you check for time differences, then coroutines are great. Much better than anything you could do with Update().

    You can't use them for everything you'd do in Update(), though, because you only get a performance benefit if the processing happens less than it would, if it happened every frame. So the greater the number you input to WaitForSeconds, the less responsive the game is going to be. It's a nice way to optimize, though. See if everything can run at a smooth framerate in Update(), and if not, start putting what's less important in coroutines, and keep kicking the values you use for WaitForSeconds up until the game is playable.
  3. n0mad

    n0mad

    Member

    Joined:
    Jan 27, 2009
    Messages:
    3,731
    That's cool advices Jessy, thanks.

    I'll see what would benefit and what would not, but except the input detection movements methods, I guess nothing else has to be checked on every single frame (for basic behaviours I mean).

    In fact I'm already running at 40 fps on 3GS, but I feel a lot could be improved via coroutines :D

    On the other hand, one thing that annoyed me at first with checkup coroutines was that while(true) statement.

    Apple clearly stated that every dev should avoid loops and Polling in general.


    Here.

    And more there.
  4. Dreamora

    Dreamora

    Member

    Joined:
    Apr 5, 2008
    Messages:
    26,586
    40 on the 3GS isn't all that great ... thats at very best 15 - 20 on the 1st generation devices and potentially the iphone 3G


    As for the event handlers: you aren't working with UIKit, so they are of no importance.
    Though what I would recommend you to do is not using any time difference measures. use yield and wait till the time has passed point ;)
  5. n0mad

    n0mad

    Member

    Joined:
    Jan 27, 2009
    Messages:
    3,731
    Well, I guess 40 fps is still pretty good for a start before any deep optimizing process, considering the high number of bones and keyframes of my chars (82 bones total, 2000 x 2 keyframes, 1024 textures), plus all the visual effects, custom HUD, physics and higly detailed background :)

    Still optimizable though, but for what I see on my handheld, it's already appearing very smooth and responsive ;)

    And I will abuse of yield, for sure :D
  6. Dreamora

    Dreamora

    Member

    Joined:
    Apr 5, 2008
    Messages:
    26,586
    Thats no wonder, your device is up to 4 times faster than the 3G and older ...
    So yes you are still looking forward to a lot of optimization or cut down of your userbase to more or less the 3GS owners

    sounds especially like you will have to work on the memory consumption of your app to make it run at all
  7. n0mad

    n0mad

    Member

    Joined:
    Jan 27, 2009
    Messages:
    3,731
    That's actually the tweaking I found to be efficient, yes, cutting textures by an half to make it run on 3G ^^
  8. Jessy

    Jessy

    Member

    Joined:
    Jun 7, 2007
    Messages:
    7,173
    For which Unity is awesome, because you can just change Import Settings instead of actually importing resized graphics.
  9. n0mad

    n0mad

    Member

    Joined:
    Jan 27, 2009
    Messages:
    3,731
    Yup I like that feature, but it's even more awesome with Texture2D.PackTextures() where you can just tell what target size you want :D
  10. n0mad

    n0mad

    Member

    Joined:
    Jan 27, 2009
    Messages:
    3,731
    By the way, what is the unit reference of "frametime" in the profiler ?
    is it the fps ?


    here is one from an actual build :

    Code (csharp):
    1. iPhone Unity internal profiler stats:
    2.  
    3. cpu-player>    min: 17.4   max: 29.3   avg: 24.4
    4. cpu-ogles-drv> min:  3.0   max:  5.7   avg:  3.7
    5. cpu-present>   min:  0.5   max:  1.8   avg:  0.6
    6. frametime>     min: 25.2   max: 43.0   avg: 33.3
    7. draw-call #>   min: 5   max: 5   avg: 5
    8. tris #>        min: 4220   max: 4220   avg: 4220
    9. verts #>       min: 6196   max: 6196   avg: 6196
    10. player-detail> physx:  0.4 animation: 11.3 skinning:  8.3 render:  1.7 fixed-update-count: 1 .. 2
    11. mono-scripts>  update:  2.1   fixedUpdate:  0.4 coroutines:  0.0
    12. mono-memory>   used heap: 2064384 allocated heap: 3960832
    13. ----------------------------------------
  11. Dreamora

    Dreamora

    Member

    Joined:
    Apr 5, 2008
    Messages:
    26,586
    all times are milliseconds.
  12. n0mad

    n0mad

    Member

    Joined:
    Jan 27, 2009
    Messages:
    3,731
    Great, thanks :)
  13. MikaMobile

    MikaMobile

    Member

    Joined:
    Jan 29, 2009
    Messages:
    810
    Your "used heap" there is pretty large. Does it go up every frame by chance? You may be performing memory allocations in your scripts every frame, which can lead to hiccups down the line due to frequent mono GC. In particular, coroutines can cause such allocations in some situations.
  14. n0mad

    n0mad

    Member

    Joined:
    Jan 27, 2009
    Messages:
    3,731
    I found it to be that high on every frame, even if I don't have any clue of how much we should aim for in a reasonable way. It was that high even before the coroutine translation.

    I guess it's because of the huge animations, each char has a 8 MB animation (40 - 50 moves per character), and 4 x 1024 texture atlases (2 chars + 1 background + 1 HUD).

    When I will reach the deep optimization process, I'll cut characters texture size by an half for sure.
    But I feel like I can't help but have a huge Animation allocation for this type of game :/



    edit @ Tagged : Don't worry dude, you're not stealing anything, I would be happy to launch (yet another) general performance thread ;)
  15. GamesByJerry

    GamesByJerry

    New Member

    Joined:
    Oct 27, 2008
    Messages:
    62
    Just in regards to this, I've notice the used heap in my game going up until the end of the background mp3 music, once that loops over and starts again the used heap drops right back down. This happens in sync with the music every time, is this normal? Or should I be looking through the code for culprits?

    Apologies for the thread stealing ;-)
  16. MikaMobile

    MikaMobile

    Member

    Joined:
    Jan 29, 2009
    Messages:
    810
    Compressed audio doesn't add to your mono heap, so if its going up in sync with your music that's just a coincidence. The only thing that should contribute to that particular chunk of memory is allocations from script - assets like textures, sounds and animations are not represented there. The actual size of the heap isn't really that big of a concern by itself as long as its size is relatively steady, but if its rising every frame you've got some kind of allocation happening in your update(s). This will eventually cause mono garbage collection to trigger, which can make your framerate stutter unpleasantly for a second or two. The severity and frequency of mono GC's effect on your framerate can vary wildly - its usually not a showstopper even if GC is running pretty frequently, but its best to avoid it for ideal performance. There's generally no reason why you'd ever have to perform allocations every frame, so its quite avoidable if you keep an eye on it.
  17. Jessy

    Jessy

    Member

    Joined:
    Jun 7, 2007
    Messages:
    7,173
    Can you tell me what might cause that to happen? For instance, would this code be improved by declaring the variables outside of Update?

    Code (csharp):
    1. void Update ()
    2. {  
    3.     int touchCount = iPhoneInput.touchCount;
    4.     bool moving = false;
    5.     bool looking = false;
    6. }
    I have been declaring variables inside of functions when they won't need to be used outside of the functions, but if that's allocating memory for a new variable each time, I will stop doing that immediately.
  18. akasurreal

    akasurreal

    Member

    Joined:
    Jul 17, 2009
    Messages:
    423
    Can anyone tell me if InvokeRepeating has the same optimization benefit as using a coroutine with yield discussed here? I find it to be a cleaner approach than using a coroutine with a while() loop.
  19. Dreamora

    Dreamora

    Member

    Joined:
    Apr 5, 2008
    Messages:
    26,586
    Well benefit is that invokerepeating gets a new clean memory each time again, no remainder from the last run.
    drawback is that it also has to reserve that again on each call.
  20. MikaMobile

    MikaMobile

    Member

    Joined:
    Jan 29, 2009
    Messages:
    810
    I know for sure that checking an animation state's values, like in the following script...

    Code (csharp):
    1.  
    2. function Update () {
    3.      if (animation["idle"].weight == 1) {
    4.            // do stuff
    5.      }
    6. }
    7.  
    ... will allocate every frame, whereas...

    Code (csharp):
    1.  
    2. var idle = animation["idle"];
    3.  
    4. function Update () {
    5.      if (idle.weight == 1) {
    6.           // do stuff
    7.      }
    8. }
    9.  
    ... does not. I've also noticed that this use of co-routines causes allocations every frame, whether it someCondition is true or not...

    Code (csharp):
    1.  
    2. function Update () {
    3.      someFunction();
    4. }
    5.  
    6. function someFunction () {
    7.      if (someCondition) {
    8.          yield WaitForSeconds (1);
    9.          // do stuff
    10.      }
    11. }
    12.  
    Meanwhile, this does not, even though it is identical in function.

    Code (csharp):
    1.  
    2. function Update () {
    3.      someFunction();
    4. }
    5.  
    6. function someFunction () {
    7.      if (someCondition) {
    8.           yetAnotherFunction();
    9.      }
    10. }
    11.  
    12. function yetAnotherFunction () {
    13.      yield WaitForSeconds (1);
    14.      // do stuff
    15. }
    16.  
    I've learned these "rules" by trial and error, and watching my mono memory closely whenever I'm running the profiler. I'm sure there are other things to avoid as well - as a general rule, I always cache any reference to any component in a variable, and never really never declare temp variables inside a function at all out of paranoia.
  21. Alex

    Alex

    New Member

    Joined:
    Nov 19, 2008
    Messages:
    122
    Have you ever run into problems doing the latter? I used to do it that way, but I started getting problems with Unity giving a null reference error for the animation state variable I set up, even though it had been assigned previously. I still have no idea what was causing it.
  22. Dreamora

    Dreamora

    Member

    Joined:
    Apr 5, 2008
    Messages:
    26,586
    removing the object, switching level, removing the animation and alike are all potential candidates for causing problems
  23. GamesByJerry

    GamesByJerry

    New Member

    Joined:
    Oct 27, 2008
    Messages:
    62
    Big thanks PirateNinjaAlliance! I know my code uses strings in animations alot and this would be the cause!

    Is there some nice wiki page or pdf detailing all the optimization tips known? I spent probably 50% of my time searching/reading forum posts over the last couple weeks trying to optimize my game, having it all in one place would really help alot of people. Not to mention being able to go back to the same page and see new or missed tips.
  24. pete

    pete

    New Member

    Joined:
    Jul 21, 2005
    Messages:
    1,640
    guessing you're destroying it or something. that'll give you a null.
  25. akasurreal

    akasurreal

    Member

    Joined:
    Jul 17, 2009
    Messages:
    423
    I meant in regards to it being more efficient than doing timing checks in an Update() like the original poster of this thread stated. Also if it is more efficient, does this hold true when checking on multiple items? For example the enemies in my game, I have it do multiple timed checks for each one in my Update() to see if a certain time has passed yet. Would it be better performance to do multiple InvokeRepeating on each enemy instead?
  26. GamesByJerry

    GamesByJerry

    New Member

    Joined:
    Oct 27, 2008
    Messages:
    62
    Do you use a separate update loop for each opponent? A few months back I switched to a single script/gameobject design that controlled all opponents. It stored all opponent references in an array to give better performance. The gameobjects of the opponents themselves contained a very minimal script just for collision/trigger handling which would pass on valid results to the OpponentAI script. I used a switch statement to ensure the collision/trigger was something I needed to process before calling the OpponentAI to save useless calls as well.

    I now have a single update() loop for my entire game. The one update loop, referred to as SceneManager ingame, calls a customized loop for the Player and OpponentAI based on the game's current state (Playing, paused, special). This method might give you a noticeable increase in speed, it sure helped me :)

    This thread made me realize I'm not using coroutines enough, I might look into using them more often!
  27. akasurreal

    akasurreal

    Member

    Joined:
    Jul 17, 2009
    Messages:
    423
    I use one single Update() loop for the entire game like you. I am checking if a certain amount of time has passed on all enemies to do things like switch frame (2d sprites), move, attack, etc. Based on this thread, I am just trying to determine if switching these to InvokeRepeating or Coroutine is a more efficient way of doing this.
  28. MikaMobile

    MikaMobile

    Member

    Joined:
    Jan 29, 2009
    Messages:
    810
    It's not the use of strings in the example I provided, its the use of the animation state. For instance, you can do the following just fine.


    Code (csharp):
    1.  
    2. var myAnimation : Animation
    3.  
    4. function Update () {
    5.        myAnimation.Play("idle");
    6. }
    7.  
    In this example, I've cached my animation component in a variable (called myAnimation) which would have been assigned through the inspector. I don't need to cache my idle animation state unless I plan to check any of its individual attributes (like weight, speed, normalizedTime, time, layer, etc.)

    On the other hand, concatenating strings is a bad idea to do on a regular basis. For instance, this would be bad if I wanted to play an animation called "idle1"...

    Code (csharp):
    1.  
    2. var myAnimation : Animation;
    3. var whichIdle = 1;
    4.  
    5. function Update () {
    6.       myAnimation.Play("idle" + whichIdle);
    7. }
    8.  
    This would work, but would also allocate memory in the process. I did this a lot in Zombieville, to my eternal regret. :p
  29. n0mad

    n0mad

    Member

    Joined:
    Jan 27, 2009
    Messages:
    3,731
    Thanks for the tips, guys.

    In a more general way, I came to find myself realizing that we should have the fewest possible "yellow" words in Update() and loops ...

    Childish me :roll:
  30. akasurreal

    akasurreal

    Member

    Joined:
    Jul 17, 2009
    Messages:
    423
    I went through and made sure I am not allocating any new variables in my Update(), yet my heap still continues to steadily rise. The only other thing I can think of is I am using some foreach () loops. Is that the possible cause, if so, that means you cannot use foreach() loops at all which would make me sad =(
  31. ChrisStoy

    ChrisStoy

    Member

    Joined:
    Apr 11, 2009
    Messages:
    8
    I'm pretty certain foreach allocates memory for the enumerator. I've made it a habit to never use foreach, which, makes me sad too.

    Chris.
  32. n0mad

    n0mad

    Member

    Joined:
    Jan 27, 2009
    Messages:
    3,731
    Here is the same build, without any change,

    before 1.5 :

    after 1.5 :

    Code (csharp):
    1. iPhone Unity internal profiler stats:
    2.  
    3. cpu-player>    min:  7.8   max: 12.2   avg:  9.9
    4. cpu-ogles-drv> min:  3.1   max:  5.1   avg:  3.4
    5. cpu-present>   min:  0.5   max: 33.9   avg: 12.6
    6. frametime>     min: 16.0   max: 47.9   avg: 27.9
    7. draw-call #>   min:   5    max:   5    avg:   5     | batched:     0
    8. tris #>        min: 11380  max: 11380  avg: 11380   | batched:     0
    9. verts #>       min:  6108  max:  6108  avg:  6108   | batched:     0
    10. player-detail> physx:  0.3 animation:  1.3 culling  0.1 skinning:  4.2 batching:  0.0 render:  1.6 fixed-update-count: 0 .. 2
    11. mono-scripts>  update:  2.1   fixedUpdate:  0.2 coroutines:  0.0
    12. mono-memory>   used heap: 479232 allocated heap: 1429504  max number of collections: 0 collection total duration:  0.0


    This is outstanding ...

    Look at the Animation Skinning values ...
    Animation was just cut by 10.

    And I didn't even try batching or other new optimizations !

    Thank you guys.


    edit : Oh snap, I just tested the game on my 3G, and it runs nearly as fast as my 3GS pre 1.5 !
    And ice on the cake : I don't know what big memory management you did, but now it runs with the same big huge 1024x1024 textures I reserved only for the 3GS ... Before 1.5, the app just crashed on my 3G if I didn't turn them to 512x512.

    This is really, really impressive.
  33. akasurreal

    akasurreal

    Member

    Joined:
    Jul 17, 2009
    Messages:
    423
    Awesome, looking forward to testing it!
  34. akasurreal

    akasurreal

    Member

    Joined:
    Jul 17, 2009
    Messages:
    423
    Update: I painstakingly replaced all my foreach() loops but it did solve the issue. My heap is not going up each frame anymore! =)
  35. GamesByJerry

    GamesByJerry

    New Member

    Joined:
    Oct 27, 2008
    Messages:
    62
    Thanks for the update! I guess that means I have a small task ahead of me ;-)
  36. n0mad

    n0mad

    Member

    Joined:
    Jan 27, 2009
    Messages:
    3,731
    Just to confirm that loops and direct reference to properties can be a severe hit to runtime performance :
    Even if my fps went up from the 1.5, I can clearly see now the few mini-stutterings PirateNinja was talking about previously. The actual very smooth fps much more separates cpu stutterings from GPU performance.

    The heap size is indeed jumping over time, as PNA predicted.

    So it's vital to remove any direct ref to "yellow" properties on runtime, and avoid as much loops as possible.



    Caching is the key ;)
  37. Eric5h5

    Eric5h5

    Volunteer Moderator Moderator

    Joined:
    Jul 19, 2006
    Messages:
    25,588
    Loops are fine. Can't do much programming without loops. ;) Just foreach loops that are problematic (AKA for...in loops in Javascript.)

    --Eric
  38. n0mad

    n0mad

    Member

    Joined:
    Jan 27, 2009
    Messages:
    3,731
    Loops are dangerous :eek:
  39. tau

    tau

    New Member

    Joined:
    Dec 15, 2008
    Messages:
    113
    Just to add my $0.02 to the coding approach to avoid the GC throttling

    Instead of using (pseudo code)
    Code (csharp):
    1.  
    2. var idle = getAnimation("idle");
    3.  
    4. function Update()
    5. {
    6.     if (idle.weight == 1) {...}
    7. }
    8.  
    I would do a lazy loading that is safe if the animation was re-loaded (set idle = null every time you re-load animation or level):
    Code (csharp):
    1.  
    2. var idle = null;
    3.  
    4. function Update()
    5. {
    6.     if (idle == null) { idle = getAnimation("idle"); }
    7.     if (idle.weight == 1) {...}
    8. }
    9.  
  40. MikaMobile

    MikaMobile

    Member

    Joined:
    Jan 29, 2009
    Messages:
    810
    I too have found that for-each loops can cause troubles, but they can easily be replaced with a loop like the following which will cause you no problems.


    Code (csharp):
    1.  
    2. for (i = 0 ; i < myArray.length ; i++) {
    3.         // do stuff to myArray[i] //
    4. }
    5.  
  41. n0mad

    n0mad

    Member

    Joined:
    Jan 27, 2009
    Messages:
    3,731


    I'm afraid that it's not quite a good idea, because in your solution, you're not caching the data anymore.
    I might be wrong, but even if you load it only one time, you still load it at runtime, which should not be the best compiled part of any code to load one-shot data.
  42. MikaMobile

    MikaMobile

    Member

    Joined:
    Jan 29, 2009
    Messages:
    810
    Tau's solution is pretty much identical to just storing an animation state in a variable in an awake function (which is what I do). He may be assigning it in an update loop, but only if the value is null.. so it only triggers once. Where exactly you assign its value is irrelevant, as long as you aren't re-assigning it every frame (which he isn't.)

    The overhead of checking whether it is null or not every frame is probably miniscule, but it exists, so I'd rather not be doing that for a few dozen animations 30 times a second.
  43. Jessy

    Jessy

    Member

    Joined:
    Jun 7, 2007
    Messages:
    7,173
    Has this foreach / for...in stuff been discussed before? I use that stuff, but if it's bad practice, then I won't ever do it again. Is this a fault of the loop method itself, or just how Unity handles it, suggesting that it could be made to behave the same as a standard for loop in the future?
  44. MikaMobile

    MikaMobile

    Member

    Joined:
    Jan 29, 2009
    Messages:
    810
    I think it comes down to if the loop has to allocate memory when it executes, but I honestly don't know for certain - figuring out what things allocate memory and what things don't has been an ongoing learning process for me. For instance, the following loop was causing a significant framerate stutter the first time it was executing in my current project.

    Code (csharp):
    1.  
    2. for (var temp : Transform in transformArray) {
    3.         // do stuff to temp
    4. }
    5.  
    I think its because of the fact that its generating this Transform variable on the fly? Again, I'm not 100% positive, and I'm kinda learning by trial and error (that is, noticing reproducible framerate hitches, and commenting out chunks of code until I figure out why its doing it.)
  45. Jessy

    Jessy

    Member

    Joined:
    Jun 7, 2007
    Messages:
    7,173
    This is very un-Unitylike, methinks. Hopefully we'll get some nice new tips at the Unite conference. Although, I am almost positively not going to get to go, so I'll have to wait several months for the videos. :cry:
  46. akasurreal

    akasurreal

    Member

    Joined:
    Jul 17, 2009
    Messages:
    423
    This is just the way .NET memory management works because foreach() is allocating an enumerator each time. Garbage Collection will eventually clear the unused memory, but in my case, it ate up a few megs first before GC hit. There is also the "stutter" that could happen in game when it does GC that you can avoid.

    I have definitely verified that this causes the heap to go up:

    Code (csharp):
    1. foreach(ArrowShot arrowShot in arrowList)
    2. {
    3.     //do stuff with arrowShot
    4. }
    5.  
    6. //assume same is true for "for( in )" in JS
    7.  
    But this does not:

    Code (csharp):
    1. //define these outside of Update()
    2. ArrowShot arrowShot;
    3. int ai;
    4.  
    5. for (ai = 0; ai < arrowList.Count; ai++)
    6. {
    7.     arrowShot = (ArrowShot)arrowList[ai];
    8.     //do stuff with arrowShot
    9. }
    10.  
  47. Jessy

    Jessy

    Member

    Joined:
    Jun 7, 2007
    Messages:
    7,173
    Totally lamezorz. :(

    I like the "as close as possible to English" method of code writing, and taking out this type of loop damages that. Oh well.

    What is the reason that anyone would even use foreach then?
  48. n0mad

    n0mad

    Member

    Joined:
    Jan 27, 2009
    Messages:
    3,731
    Dunno ... I can't see another unchangeable use than for enumerating declarations, but that would be in Awake() :roll:
  49. akasurreal

    akasurreal

    Member

    Joined:
    Jul 17, 2009
    Messages:
    423
    I definitely don't plan to use it anymore, that's for sure. And yes I agree that it really stinks as I prefer the code legibility of a foreach () loop. You could use it in initialization like start() or awake() I suppose. You could also choose to use them anywhere as long as you weren't concerned about the GC creating a stutter every once in a while. Depends on how fast you increase heap size on how often this would happen. Some games wouldn't really be phased by the cleanup I imagine as opposed to something more action-based.

    The memory benefit is probably also unique per game. My heap size rarely goes over 0.5MB by the end of a level now and I manually call System.GC on a new level load, so I am saving about 2.5MB of memory over before I started to watch the heap. Verified this by watching Real Memory in activity monitor too.

    P.S. Anyone know if Unity 1.5 mono optimizations helps this issue? I had already finished changing all my allocations just before it released so I have no easy way to test anymore.
  50. Eric5h5

    Eric5h5

    Volunteer Moderator Moderator

    Joined:
    Jul 19, 2006
    Messages:
    25,588
    Because it's simpler and faster to type in those cases when you just want to do something to every item in an array and don't care about the index, and you're not using Unity iPhone. :)

    --Eric