- Home /
How can code generate memory garbage without instantiating any memory?
I've got the following code snippet, and the Unity profiler is saying that Tile.UpdateWater() is generating about 0.6mb of GC Alloc per frame (it's called 131,000 times per frame). But it does not instantiate any arrays or classes, and it does not use strings at all.
I've tried removing chunks of code, but I couldn't track it down through that method either because it would allocate fully one run, but allocate nothing the next run on the same code at times. It's really driving me up a wall.
I've also tried pulling this function out into it's own static class with all local variables predefined as static class variables, but that only seemed to make it worse.
The only local variables being defined are ints and floats, but those shouldn't trigger allocations, right?
public class Tile
{
public enum Modes
{
Nothing,
FloorOnly,
FloorWall,
FloorStairs,
FloorRamp,
WallOnly,
StairsOnly,
}
private float WaterAmt;
private Tile North, South, East, West, Below, Above;
public Modes Mode;
public bool HasWall()
{
return Mode == Modes.WallOnly || Mode == Modes.FloorWall;
}
public void UpdateWater()
{
if (WaterAmt > 1)
WaterAmt = 1;
if (WaterAmt <= 0)
return;
if (HasWall())
{
WaterAmt = 0;
return;
}
float maxFlowAmt = Mathf.Min(0.5f, Time.deltaTime * 5);
if(Mode == Modes.Nothing && Below != null)
{
if (!Below.HasWall())
{
float toDrop = Mathf.Min(1 - Below.WaterAmt, WaterAmt);
Below.WaterAmt += toDrop;
WaterAmt -= toDrop;
}
}
if (WaterAmt <= 0)
return;
int ct = 1;
float totalWater = WaterAmt;
if (West != null && !West.HasWall())
{
ct++;
totalWater += West.WaterAmt;
}
if (East != null && !East.HasWall())
{
ct++;
totalWater += East.WaterAmt;
}
if (South != null && !South.HasWall())
{
ct++;
totalWater += South.WaterAmt;
}
if (North != null && !North.HasWall())
{
ct++;
totalWater += North.WaterAmt;
}
float avgWater = totalWater / ct;
if (ct <= 1)
return;
if (avgWater >= 0.9999999f)
return;
if (avgWater < WaterAmt)
{
float needW = 0, needE = 0, needN = 0, needS = 0;
if (West != null && !West.HasWall())
needW = Mathf.Max(avgWater - West.WaterAmt);
if (East != null && !East.HasWall())
needE = Mathf.Max(avgWater - East.WaterAmt);
if (South != null && !South.HasWall())
needS = Mathf.Max(avgWater - South.WaterAmt);
if (North != null && !North.HasWall())
needN = Mathf.Max(avgWater - North.WaterAmt);
float totalNeed = needW + needE + needN + needS;
if (totalNeed > 0)
{
float maxToGive = WaterAmt - avgWater;
if (totalNeed > maxToGive)
{
needW *= (maxToGive / totalNeed);
needE *= (maxToGive / totalNeed);
needN *= (maxToGive / totalNeed);
needS *= (maxToGive / totalNeed);
}
if (needW > 0)
{
float amt = needW * maxFlowAmt;
West.WaterAmt += amt;
WaterAmt -= amt;
}
if (needE > 0)
{
float amt = needE * maxFlowAmt;
East.WaterAmt += amt;
WaterAmt -= amt;
}
if (needN > 0)
{
float amt = needN * maxFlowAmt;
North.WaterAmt += amt;
WaterAmt -= amt;
}
if (needS > 0)
{
float amt = needS * maxFlowAmt;
South.WaterAmt += amt;
WaterAmt -= amt;
}
}
}
}
}
Okay, adding a few details here hoping someone can help out.
I've been developing C# in .NET for over 5 years now, so I've got a pretty good grasp of how the GC process is supposed to work. I'm starting to think this is an issue with the $$anonymous$$ono GC (wouldn't be the first time).
Here's the profiler screenshot: So, it's definitely in that specific function that the GC Alloc is co$$anonymous$$g from.
Here's the surrounding invoking code:
for (int x = 0; x < SizeXZ; x++) { for (int z = 0; z < SizeXZ; z++) { for (int y = 0; y < SizeY; y++) { TileData[x][z][y].UpdateWater(); } } }
So it's not using linq, to the best of my knowledge. I'm really stumped here.
Another note: I added a return; statement to the beginning of the function, so it would still be called, but not execute. The problem went completely away, so it's definitely in this function; I just can't figure out where.
Huh, good point. I think those are remains of keeping it clamped above zero, but the params may make an array.... huh. That could be it. Let me run some unit tests.
Holy crap, Bonfire Boy, that was it. I ran a unit test loop calling $$anonymous$$athf.$$anonymous$$ax 100,000 times per frame. When using two parameters, it generates no garbage. When using one or three, it generates about 5mb per frame.
So, that's a new one to add to the $$anonymous$$ono GC watch list: params[] functions.
Please put that in answer form so I can mark it as the correct one!
Holy crap indeed. I nearly didn't post the comment because it felt like trivial nitpicking, the array creation only occurred to me as an afterthought once I'd typed it in. I've converted it to an answer.
Answer by Bonfire-Boy · May 12, 2015 at 03:24 PM
Having pored over that code for a while and come up with nothing, I wanted to make that time feel a little less wasted by pointing out that you have a few redundant calls to Mathf.Max in there (where you're passing in a single float parameter, which invokes the Mathf.Max( params float[] values )
version of the function to give you the max out of an arbitrary number of floats, but there's only one so it always just returns the one you give it).
And then (and this is a bloody long shot) it occurred to me that maybe it's wrapping that single parameter in a new array under the hood..?
Bingo. Don't use params[] functions in large loops or you'll generate garbage.
Answer by fjhamming_CleVR · May 11, 2015 at 09:52 AM
Well. It seems you have a few other local variables: private Tile North, South, East, West, Below, Above;
These are instances of the same class which recursively add instances of the same ints, floats and any other variables you declared in the class (like tiles, which do the same until infinity)
Try making these variables static using (not sure if that is sufficient to make the entire rule static). private static Tile North, South, East, West, Below, Above;
Yes, the tiles are all created at runtime, then destroyed when the game is closed. Those class variables are references to a larger array, not instantiated from within the Tile class, and certainly not instantiated within this function. Since they are reference types, they should only add 4 bytes to the class footprint each, as I understand it, and should cause no additional 'recursive adding'. Plus, the function never calls itself, so it is not, by definition, recursive. It's called so many times from it's handler's array.
You definitely need to search in reference variables anyway since primitives, enums and structs do not cause any gc alloc at all (at least i think they are allocated to the stack which is not managed by GC). Does your call stack involve linq by any chance? Or some other lambda expression that could cause the mono JIT to create wrappers at runtime?
I don't believe it uses linq, just a for loop through a three dimension array to call this function. But even if it did, wouldn't the Unity profiler show the GC Alloc in the calling function with the loop if that's the case? Here it is pointing the finger at this function.
No lambda or delegates are used in this process. The Tile references are definitely instantiated at the beginning, shortly after the array is populated with Tiles, and should never be set again. The code above is in the update loop.
That's what's driving me nuts though. It only instantiates primitives, but it's creating garbage!
I'll post some surrounding code and a screenshot of the profiler in about an hour when I get home.
Answer by Pangamini · May 11, 2015 at 07:53 AM
Can't tell from code, but you can find out easily by running your code in "deep profiler"
I used the deep profiler to find out the problem was this function. It mentions a few other sub-functions (like $$anonymous$$athf.$$anonymous$$ax) but none of them generate the memory; only the main UpdateWater() function. I'll post a screenshot of the profiler this evening.
$$anonymous$$aybe you could try to disassemble the function and see if there are no allocations produced by the compiler
Answer by Joris-V · May 12, 2015 at 09:53 AM
Are you sure you are not creating a lot of Tile objects at runtime that reference to each other? This construction could possibly cause the memory leak. I would suggest you make the object IDisposable and in the dispose method remove all North, South, East, West, Below and Above references. Do not forget to call the Dispose method on the Tile object before you lose the last reference to a Tile object.
In the following example the IDisposable pattern is implemented as described on https://msdn.microsoft.com/en-us/library/system.idisposable%28v=vs.110%29.aspx. Combine this with your code above.
public class Tile : IDisposable
{
private bool disposed = false;
~Tile()
{
this.Dispose(false);
}
public void Dispose()
{
this.Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (this.disposed)
{
return;
}
if (disposing)
{
// Free any managed objects here.
this.North = null; // Note that you also need to call dispose if you don't have any other references to it's dispose.
this.South = null;
this.East = null;
this.West = null;
this.Below = null;
this.Above = null;
}
// Free any unmanaged objects here.
this.disposed = true;
}
}
I'll try it, but I don't think that's the problem. The tiles are all instantiated at load time, and survive the life of the scene. There should be no instantiation or destruction during the normal game loop. I'll try throwing some Debug.Logs in the constructor and dispose functions to see if they are erroneously, but I doubt it.