It's official, foreach is bad (in Unity)

Comments

  • Tuism said:
    What? So the web player won't need a plugin, just the native browser stuff? And it'll be just as fast? Really?
    This has been their goal for ages with all their work on NaCl and WebGL, AFAIK. Been a long time coming. Hopefully it's the final nail in Flash's 10 year-old coffin. But it probably won't be. :(
  • Flash is pretty close to dead already, I (thankfully) see far less of it around these days.
  • Erm, just wanted to ask, if I use foreach in my stuff now, would I:

    1. Crash my game eventually due to memory leak?
    2. Go to gamedev hell?
    3. Sell millions because my game has controversy value?
  • 1) No. The garbage collector will just run more frequently and have more work to do.
    2) No. You'll just get a smack on the knuckles with a ruler.
    3) Only with a much bigger controversy than foreach loops unfortunately.
  • edited
    Nice necro @Tuism!

    Though if you'd waited 11 more days it would have made it a full year :(

    (Having said that, I think I'm going to do a pass soon where I improve our garbage collection overhead in Broforce)
  • edited
    Well I was just wondering what the outcome of this is as I'm... using foreach. I asked on twitter if this was fixed and apparently Unity is replacing mono in a non-descript and uncommitted time frame... So no.

    Thanks @ChristopherM :)

    And @BlackShipsFilltheSky that's premature optimisation, son :P
  • edited
    Tuism said:
    And @BlackShipsFilltheSky that's premature optimisation, son :P
    You wouldn't say that if you saw our profiler :) (or knew what someone's going to have to go through to make the thing run on PS4)

  • Tuism said:
    And @BlackShipsFilltheSky that's premature optimisation, son :P
    You wouldn't say that if you saw our profiler :) (or knew what someone's going to have to go through to make the thing run on PS4)

    Was just kidding!! XD

    And is it a beast to get running on a monster of a machine? It's not because of optimisation, right? More due to different architecture?
  • edited
    @Tuism The problem with foreach, to clarify is that you're making garbage for the garbage collector to sweep up. So that means garbage collection is triggered more often. And because the garbage collection is slow, and depending on a lot of things (like are you on a constrained platform like mobile) that can cause your game to lag sporadically - which might be game development hell when you're trying to figure out why?

    So generally the wise programmer tries to avoid creating any sort of garbage while the game is running, because if the GC never runs you are guaranteed to never have any skips. (At set up time though it's fine, because you're only paying the price once). That's not to say you will have issues if you do create garbage (using for each), but is a lot of "it depends" that programmers are never a fan of ;)

    Eventually this will be less of an issue when Unity upgrades the GC to one not from the stone ages. But I wouldn't expect that any time before Unity 6. ;)

    Edit: To build a better intuition of this you can actually see when GC runs in the unity profiler. And you'll notice it's usually a very big spike. If you have enough overhead that's not an issue, otherwise it will kill your framerate. If you have GC problems, you can see how much memory each method call is allocating every frame. When you get that down to zero the GC spikes will stop. Generally string and the old OnGUI methods are fantastic ways to spew loads of garbage. Way more than a humble for each loop. It's all about relative tradeoffs. There's no point saving the memory of for each if other things are generating orders of magnitude more memory each frame ;)
    Thanked by 3Tuism Chippit mattbenic
  • With IL2CPP already being a thing on iOS, I suspect that this foreach thing is no longer a concern on that platform. On PC it's rarely a big deal thanks to generational collection anyway.

    Maybe it would be a good idea to compare how worried people should be about this particular thing onto some sort of scale to prevent against obsessing about tiny amounts of garbage generation super early in a project when it's usually not a concern? This seems to be a big focus for @Tuism right now and I'm not convinced that it's a good time investment, TBH.
  • Nono, I understand and agree, I'm still building stuff and using foreach, the thought just crossed my mind and I just brought it up because, well, thought of it. I'm not spending any amount of time "optimising" besides practicing pooling in this thing I'm making.

    If all goes right I'll have a rough prototype to post soon :) Showed it at Jozi meetup already and it wasn't quite ready yet (as a "vertical slice" nog al)

    Don't worry, I'm not obsessing, I guess it's just cos I'm asking different kinds of questions more recently. Thanks @dislekcia :)
  • With IL2CPP already being a thing on iOS, I suspect that this foreach thing is no longer a concern on that platform. On PC it's rarely a big deal thanks to generational collection anyway.
    I can't comment on newer iOS/IL2CPP, but on PC the whole point is that Unity (currently) doesn't use a modern GC. They're still using a version of Mono from just before their big GC upgrade. There's talk of that changing in an aupcoming version, but there are technical and licensing hurdles before it's achieved.

    Something else to consider (even when you are at the point of profiling your game for final performance improvement) is whether you're making the sort of game where GC will even be noticeable. If you're building something where a constantly high framerate doesn't matter, say a board or card game, you can easily get away with forcing a GC on every turn or something. It's lazy/sloppy, but it really doesn't matter. When you're building something where there is constant on-screen movement though, that quick drop can be extremely noticeable-especially when it affects input.
  • edited
    Everything I've read from everyone writing on GC is that calling GC.Collect is the best way to shoot yourself in the foot. I figured the turn-based argument would benefit from it, but even then people say, don't do it.

    True or not true?

    That said, I'm not going to prematurely optimise :P

    And yes, I've noticed those quick drops SEVERELY with my non-turn-based games. I would consider them unplayable if it happened once a minute.
  • edited
    Everything I've read from everyone writing on GC is that calling GC.Collect is the best way to shoot yourself in the foot.
    Then my foot should be full of holes :) Please share some of those references.

    I use manual GCs, but only in sensible places, like just after showing a pause menu. Eg after we load a scene in Unity, we do the following (keeping in mind this is in 3.5.x):
    AsyncOperation async = null;
    # if !UNITY_EDITOR
                async = Resources.UnloadUnusedAssets();
    # endif //!UNITY_EDITOR
                while (async != null && !async.isDone)
                {
                    yield return async;
                }
                System.GC.Collect();


    Obviously the ideal is to minimize allocations. If you can cut them out completely, that's great and good for you, but it's often just not worth the effort. If you can make some reasonable assumptions about your gameplay and the frequency of time between menus and scene loads, then you can work how how much is an "acceptable" level of allocation that won't affect your player with unwelcome GC jerks.
  • What what? What's <a href> doing in unity code? Is that supposed to be there or is it a paste error?

    Should be this right?

    AsyncOperation async = null;
    #if !UNITY_EDITOR
                async = Resources.UnloadUnusedAssets();
    #endif //!UNITY_EDITOR
                while (async != null && !async.isDone)
                {
                    yield return async;
                }
                System.GC.Collect();
  • edited
    Doh, it seems a hash triggers a link in the forum software :P (which wasn't there in preview, BTW)

    [edit]
    Ok, so adding a space in there sorted it, you'd need to correct for that if copy-pasting the code
    [/edit]
  • Lol, yeah it wasn't there in the preview, as you can see my code also went wonky :)

    Thanks! :D
  • @dislekcia, based on my understanding and my conversations with Lucas (the unity dev who stopped by at the meet up last month) that's not the case. Certainly on PC we don't have access to a generational GC, and on IL2CPP they've laid the groundwork for a GC upgrade but it hasn't happened yet.

    Granted though your advice to Tuism is good advice, but if you're making games for commercial release it's good to know about where the bottom of the pit actually lies.

    @Tuism strategically calling System.GC.Collect is something I can see a downside to? In fact we do this in Cadence after every level load, becuase it by necessity generates a lot of garbage - new data has to come from somewhere ;)
  • Granted though your advice to Tuism is good advice, but if you're making games for commercial release it's good to know about where the bottom of the pit actually lies.
    Meh. I don't think it's as important as this thread makes it out to be... It smacks of the whole "Known problems of small impact cannibalising unknown problems with large impact" thing. Because we know about GC potentially being a small slowdown sometimes, it's easy to focus on that instead of doing things like testing your release system (because that should just work, everyone else's does, right?) or having all your communication material lined up ready to fire.

    If you're making games for commercial release there are hundreds of things that are more worth spending time on than worrying about GC stuff (beyond maaaaaybe setting up okay coding habits up front). Excess garbage is also usually a pretty easy thing to fix, but a really hard thing to diagnose: Lowering your garbage generation is a matter of chasing down allocations and being more efficient but it's also a rabbit hole that is difficult to escape from until you've gone all the way to C++. But every time I've actually focused on garbage, it's been because I misdiagnosed the cause of a regular stutter... One time on Xbox it was stuttering due to too many graphics state changes and optimising our draw call order fixed that. Another time it was the slowness of instantiating empty gameobjects on the fly and switching to prefabs fixed that.

    So I guess my point is this: If you're actually going to spend time optimising your GC calls/garbage amount, make damn sure that that's the real cause of your problem through rigorous testing of over-generating garbage. Don't just assume it's a GC thing and then hike down the marginal optimisation path-of-no-return until you hit some magical threshold that you hope is going to remove the hitch. That should be obvious, but it's also what nearly every GC optimisation spree looks like.
    Thanked by 1hermantulleken
  • So I guess my point is this: If you're actually going to spend time optimising your GC calls/garbage amount, make damn sure that that's the real cause of your problem through rigorous testing of over-generating garbage. Don't just assume it's a GC thing
    Yeah, of course, profile before you make any assumptions about what is causing an issue.
    Meh. I don't think it's as important as this thread makes it out to be...
    Assuming you mean garbage in general here, not foreach specifically: In a managed environment that uses an non-generational garbage collector (ie Unity, or XNA on X360 or WinPhone), in any game where occasional framerate hitches will affect player experience, it can be. At least that has been my experience on every game I've worked on under those constraints. It's certainly not the One Big Thing you need to worry about, but it's one of those many things worth keeping at the back of your mind.
    Thanked by 1Chippit
  • Lowering your garbage generation is a matter of chasing down allocations and being more efficient but it's also a rabbit hole that is difficult to escape from until you've gone all the way to C++
    So that's exactly what this discussions is about avoiding for me. Sane coding habits that avoid ever having to go near that rabbit hole. ;) Fortunately the Unity profiler is pretty ace and makes the rabbit hole much shallower if ever the problem does arise - that massive GC spike every time your frame stutters is a dead ringer that's hard to misdiagnose ;). I still have nightmares about trying to get AIR (flash) to behave on mobile. Tools really do make a massive difference. Knowing they exist and what they are trying to tell you isn't a bad thing.
  • edited
    GC is one of those things I don't miss. Better never to have to guess at the antics of something you didn't code, from day one. As for the language, foreach is another such, and then there are things like yield and delegates which never needed to feature and simply put one more thing "under the hood" and out of reach. The more of these features that get tacked on to the original core language, the more potentially wobbly ground beneath your feet. I've always really wanted to like C#, and there are parts I love (like the generics and collections). But it's ultimately a love / hate relationship when I see how much simpler some things could probably have been. Java still feels cleaner, even if it's a decade behind.

    That's the price paid for ease in other areas. C can be a PITA sometimes, but you know that any mistakes are your own. You have the option to improve around those mistakes, architect better for next time. And the language is tight.
  • I spent tons of time in C++ (as I mentioned in your thread), and all it did was make me more of a C# fanboi :P

    Even back in C++ days, there were always devs that shied away from new concepts that they didn't trust, for fear of performance implications (classes, templates, etc). Very often, the little bit of time spent researching how to use these correctly, and the implications of using them incorrectly, leads to days or months saved in productivity because those features often make code more maintainable..

    That's why I love C# and .Net, really, I find the language introduces so many useful techniques (like GC and foreach and delegates) and the framework has so many well understood and tested utilities (like collections and, well, everything) that code quality and maintainability is just easier to ensure.

    I get what you're saying about GC, I've written custom memory managers (some of the best fun I ever had as a programmer) and I appreciated that sense of control. But honestly, I find a better understanding of a few simple rules to keep garbage generation down is just fine and easy to teach, and infinitely less painful than monitoring a C++ codebase for bad pointer use.
  • Yeah, I'm in the "a few simple rules to minimise garbage generation" camp myself :)

    Also, delegates rock. I ended up building an entire engine around the concept because it let me do things like recompile source on the fly in-engine.
Sign In or Register to comment.