Premature Optimization: Actually a Good Habit

Premature Optimization: Actually a Good Habit

Optimizing vs. Overcomplicating

I have noticed that it has almost become a sin to care about performance early on in development, and that it’s often seen as a bad habit… But is it really? At what point do all these little optimizations become obstacles? When does “thinking two steps ahead” actually turn into overcomplicating things?

I always try to apply best practices and write readable code when I work on my tasks. They are usually based on my experience, and have grown to become instincts at this point.

However, when I work with others, I often get the impression that they think I’m overcomplicating things. I get told that “the compiler will optimize it anyway”, or that “it works as-is”. While I understand where they’re coming from, there’s no reason to ignore potential performance issues just because it works.

Compiler Optimizations

I prefer to work in a debug build, where my code is what runs and the compiler skips optimizations. This is very useful for debugging, since it allows me to see exactly what’s going on using the Visual Studio debugger. Though, it also means having to mind the performance a little more.

One recent experience I had was with a system that processed instantly in Release mode, but took 15 seconds to complete in Debug mode. I asked the person who wrote it if they knew about the performance in Debug builds, and they said they didn’t, because everybody usually runs Release builds.

A fix was quickly implemented, cutting our loading time down to less than a second in Debug mode. The simplest solution to this is of course to do your best to write efficient code in the first place, but it’s a great example of how performance can be easily overlooked if you don’t keep it in mind.

“That’s unnecessary, leave it be!”

To be fair, it’s easy to miss what you’re not looking for. Game programmers mainly focus on gameplay and features, while system programmers focus on the engine and performance. I happen to be particularly interested in performance, so I keep it in mind at all times.

I had noticed that our main component update loop was doing a lot of unnecessary work. Our Actors list wasn’t cache-friendly, and components were being updated even if they didn’t need to be. I proposed some ideas to make it more efficient, and was met with:

“that’s unnecessary and already works as is, leave it be!”

They weren’t wrong. It works as is, and we’ve even been praised for our performance. But why stop at good, when we could easily make it even better? In the end, there were more important things to focus on. The main goal is still making a fun game, so I got back to work on my assigned tasks.

Performance vs. Convenience

Not every optimization is worth letting go of, though. There are cases where you may want to optimize for performance at the cost of convenience. For example, SIMD (Single Instruction, Multiple Data) is a technique that allows you to perform the same operation on multiple variables at the same time, which is perfect for everything math-related. It often leads to better performance, but can be more difficult to read and understand.

In Spite: Blood and Gold, we switched our Matrix4x4 class to use SIMD instead of individually multiplying each component. This made our game run a lot better, but cost us some readability. It really didn’t matter though, since it’s just a bunch of core math operations in a math library that we’ve basically solidified at this point.

Obviously, you shouldn’t sacrifice readability for performance. If your code is readable, it most likely is performant.

For example, caching variables makes your code both more readable and more efficient:

c++
// Not cached:
if (GetComponent<SomeComponent>() != nullptr)
{
	GetComponent<SomeComponent>()->DoThing();
	GetComponent<SomeComponent>()->DoAnotherThing();
}
 
// Cached:
SomeComponent* someComponent = GetComponent<SomeComponent>();
if (someComponent != nullptr)
{
	someComponent->DoThing();
	someComponent->DoAnotherThing();
}

Sure, it’s more rows and the uncached variant will likely be optimized by the compiler (depending on the setup), but it’s undoubtedly more readable, more efficient in non-optimized builds, and makes future maintenance easier.

There are also cases where you may want to optimize for readability at the cost of performance. Like in frequently touched code, where avoiding using SIMD for the sake of readability can be beneficial (especially if it’s not called very often).


Conclusion

Thinking about performance and readability doesn’t necessarily mean overcomplicating things. However, when optimizations are meaninglessly applied, they become obstacles, especially if it takes more time to implement than if you just made a simpler version.

In short, premature optimization is a good habit to have. It makes things smoother in the long run, but it shouldn’t become an obsession. Otherwise, you might end up spending more time optimizing than progressing.

Denis
Codreanu