Thursday, December 5, 2013

Unity Optimization Myths

Something has been bugging me as of late. It comes from the darkest corners of the Unity development community.
I'm talking about misguided optimization.

It astounds me just how many bizarre "tips" I've seen for making Unity games run faster or smaller. Today I'm going to be listing a few, and talking about why I disagree with them.

Mipmaps Are Bad

I've seen a blog or two mentioning that mipmaps take up more memory than just the texture itself, and therefore disabling mipmaps is better.
The truth is, yes mipmaps do take up memory. They do not, however, take up much.
With the next generation of consoles released, the now-last generation of consoles seem dated now - and it's because they have been for years. For instance, the Xbox 360 features 512MB total RAM - this RAM is shared by CPU and GPU.
As consoles become more dated, mobiles are quickly catching up. For instance, the Nexus 7 features 1GB of RAM. While much of it is taken up by the OS, that still leaves a bit more than the 512MB offered by the Xbox 360.
Despite the 512MB limitation, mipmaps are still used quite heavily on the Xbox 360, and even megatextures as seen in the first-person shooter RAGE.
Mipmaps can also speed up rendering, as they can increase locality of texel access and reduce cache thrashing (especially important as GPUs tend to have small caches).
In general, if you need to reduce VRAM usage, reducing texture size or reusing textures is what you'll want to try first, before you consider disabling mipmaps.

Lower Poly Is Better

Many people love to advocate low poly counts. It sounds like it would make sense - fewer triangles and vertices is less for the GPU to process, right?
However, these days, GPUs are incredibly fast at processing raw geometry, even mobile GPUs. That's not to say you should go wild with polygons - you'll still want to keep polycounts reasonable, but don't break your brain trying to reduce polys - you're far more likely to encounter issues with draw calls and fillrate long before you hit poly limits.

Always Keep Temporary Variables

This one I suppose is borne out of a misunderstanding of the GC and how .NET handles memory. I've seen people suggest to keep temporary variables whenever possible to reduce memory allocations, such as turning this code:

void Update()
{
 Vector3 x = calculateSomething();
}
Into this:

Vector3 x;
void Update()
{
 x = calculateSomething();
}

Not only is this untrue, it can also make code significantly less readable. Atomic types like floats, ints, and bools, are not garbage collected. They are allocated on the Stack, whereas Objects (instances of a Class) are placed on the Heap. The Stack is not garbage collected, but the Heap is. Allocating value types will never trigger garbage collection, allocating objects will.
Keeping value types as temporary variables will not afford any performance improvement whatsoever.

Use FixedUpdate

This one is kind of mixed. The idea is good, the implementation is not.
Some people have mentioned getting performance improvements by moving some logic, such as AI, to FixedUpdate rather than Update, and modifying the FixedUpdate timestep.
Thing is, FixedUpdate is where physics is supposed to take place. That's why it runs on a fixed timestep - so that you don't get odd behavior like tunneling when framerate drops.
In order to afford any benefit, your fixed timestep has to be smaller than your frametime. So if our game is running at 30 FPS, our FixedUpdate would have to run less than 30 FPS in order to afford any improvement whatsoever. If you use physics, this increases the likelihood of tunneling and other odd behaviour.
The better option, if you absolutely need to reduce how often code executes, is to keep track of a frame counter and only execute your update code once every 'n' frames (which is perfectly acceptable for AI routines). This keeps physics timestep independent from AI timestep, as it should be.

Declare Foreach Iteration Variables Beforehand

And lastly, I just today encountered a discussion on foreach and how it allocates memory. Someone suggested that, in order to eliminate the allocation, you should declare the iteration variable before the loop.
That is completely and utterly false (luckily someone more knowledgeable was present to correct the original poster of the suggestion).
The reasoning was that this:

foreach( SomeClass x in SomeCollection )
{
 // do stuff
}
Would allocate "SomeClass x" for each entry in the collection (it doesn't). And that this:

SomeClass x;
foreach( x in SomeCollection )
{
 // do stuff
}
Would fix the allocation (it doesn't).
Consider the situation where we have this instead (a perfectly plausible situation):

foreach( Vector3 v in SomeCollection )
{
 // do stuff
}
This still triggers a heap allocation. Not because it's allocating Vector3's (remember, those are stored on the Stack, not the heap), but because it's allocating an enumerator to iterate over the collection.
The correct solution is to use a regular for loop instead of a foreach. Note that you don't have to do this everywhere - just where it counts.

And that's biggest secret of optimization. You should focus your efforts on optimizing where it counts. If your login routine generates GC calls, it doesn't matter that much - because login doesn't happen regularly during the lifespan of the game, certainly not every frame, but AI or gameplay for example are much better candidates as they run frequently.

Anyway, those are my thoughts for today. I'm tired.
Happy game development, everybody.

No comments:

Post a Comment