Search Unity

Discovered A Cheap Work Around For Annoying Garbage Collection

Discussion in 'Scripting' started by brilliantgames, Jul 5, 2018.

  1. brilliantgames

    brilliantgames

    Joined:
    Jan 7, 2012
    Posts:
    1,937
    So I've been working on a massive scale crowd technology for some time now. It seems, when you are working with this much data in threading, there is nothing you can do to stop the garbage collector from coming through once in a while when you have tens of thousands of characters making complex decisions. Until now.

    I found a strange work around. By creating a large array, many times at the start of the game, it causes the garbage collector to interrupt far less often, and even seemed to reduce the lag time. I created an array of floats at the size of 10,000, 10,000 times at the start. Before I did this, the garbage collector was causing a lag every 15-20 seconds, after I implemented the array generator, it often wouldn't kick for as much as 2-5 minutes, and the lags seemed less noticeable too.

    Anyhow, I thought I would share this strange work around with the community. Anyone have any idea why GC behaves this way? Is there any way to do this without having to create an array a bunch of times?
     
  2. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,532
    Yeah, what's going on is that when you generate all those arrays at the same time the mono/.net memory manager can't find space with in the heap to put them all. So instead it asks the OS for more memory, and the OS gives it over. Then mono/.net never releases that memory because it assumes if you used it for that much memory once, you might need to again. So now there's a giant heap, and a giant heap is easy to find room in to fit regular old objects that are only in the 10's of bytes in size.

    This is a result of how the memory manager prioritizes memory allocations. It'll try to use what memory is in the heap by reorganizing it and throwing out unused objects before asking for more memory. It does this so that it doesn't balloon in system memory.

    There is a down side to this. You've effectively allocated about 400 megabytes of memory just for the heap alone (100,000,000 4 byte floats = just shy of 400 megabytes). There is also the memory needed for all the assets, the mono/.net runtime, the stack, and anything else unity needs. This isn't particularly an issue on say a desktop where people regularly have 8 gigs or more of RAM. But it can be a significant issue on say a mobile device where memory availability is significantly lower.
     
  3. brilliantgames

    brilliantgames

    Joined:
    Jan 7, 2012
    Posts:
    1,937

    That's what I figured. This is for high end PCs. So is there anyway to manually allocate memory without having to do this silly work around?
     
    MitchStan likes this.
  4. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,532
    Honestly, I don't know a way to do so.

    I know how to allocate unmanaged memory with the Marshal class, but not managed memory.

    Personally I'd just create an array of doubles (where a double is 8 bytes) to the size you want. So if you made an array 128 in length, that's 1 kilobytes. 131072 in length, and you have 1 megabytes. 134217728 in length, and you have 1 gigabyte. Then set the array null and call GC.Collect. That should do it.
     
  5. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,532
    With that said, I just tested it. And well... I'd advise against doing this.

    I have 32 gigs on my machine. And when I ran this to allocate over a gig of memory... well it allocated the first gig. But any subsequent calls started throwing 'OutOfMemoryException's, and more annoyingly I'm now sitting at 20 gigs of used memory on my system:
    memory_usage_alt1.png

    Yet my user is only at about 5 gigs, and opening the resource manager doesn't show where all this memory is going (I do have 1 virtual up, but it's not using that much memory... I only allot it 2 gigs).

    So yeah... probably a glitch in the old memory manager used by mono/.net in unity which causes a system memory leak of some sort. Which I mean honestly... no big surprise, you really shouldn't be allocating multiple gigs of memory like that but rather using streams instead.

    Time to restart now and free up all that memory.
     
  6. brilliantgames

    brilliantgames

    Joined:
    Jan 7, 2012
    Posts:
    1,937
    That's pretty crazy. My method isn't really taking any extra memory. I'm creating a much smaller array a whole bunch of times. I found I wasn't getting much noticeable improvement by just creating one large array. Instead I recreate an array of just 10,000 floats 10,000 times over. My application was only using 800mb of memory with a massive battle of 20,000 AI on a navigation grid of 2000x2000.
     
  7. snacktime

    snacktime

    Joined:
    Apr 15, 2013
    Posts:
    3,356
    The correct solution is to use the right caching. It can be done it's just a matter of knowing what approaches to use for what types of problems.

    What you are likely seeing is that changing the memory footprint changed the GC behavior. There are a lot of variables that come into play and the default behavior in .Net GC's is always bad. Mono is more bad:)

    Heap size impacts a number of things, you likely forced the heap size to increase and/or forced the GC into another mode where it's handling some things differently. GC also has a significant amount of static overhead per collection. If you can tune collections to happen at specific times based on size and age, it can make a huge impact.

    With runtimes like the JVM you can do that, and tuning often results in behavior similar to what you are seeing. It's actually quite logical. MS decided to make the GC a black box, it thought it knew better how to tune it for your app. If you have worked with runtimes like the JVM where they expose all the knobs, it's very quickly apparent that MS was dead wrong on this. And that's basically what this is, the GC working sub optimally for your specific workload. Or with your 'fix', forcing it into a mode where it's working more optimally.