When developing software, we have to remember that our number one priority is to meet our customer’s needs – i.e., making our software usable. Performance is often a critical factor in usable software. Let’s see how we must consider performance in each phase of development to make our customers happy. (You go through these phases whether you’re following a waterfall or iterative development style.)
Understand your use case and performance needs in the requirements phase
The number one rule is to understand what your customers need. Customer needs will drive how much you need to worry about performance. Your priority should be making your customers happy and making something efficient that customers won’t ever notice only takes time away from features they care about.
Common things to consider that impact performance:
- Latency requirements – how fast does a transaction/frame need to be?
- Transactions per second – how many requests are you getting every second?
- How much data will we be storing?
- How many errors can we tolerate?
- How will we measure it?
If you’re making a game, you’re likely to want 60 frames per second as your goal. 60 FPS could be complicated for a blockbuster AAA game in 4K, but if you’re making a 2D game for PC that doesn’t have many complex interactions, you won’t need to worry about performance much.
If you’re making a backend system that doesn’t get run much and no human is waiting on the output, there might not be a reason to make it extremely efficient. (Cost could be a legitimate reason you may worry about.)
If you’re making a website or app that humans interact with, you’re going to want it to be faster.
- Humans feel .1 second is instantaneous
- More than 1 second will interfere with flow
- 10 seconds is the limit for keeping human attention
Watch out for typical bottlenecks in the architecture phase
Bottlenecks are what drive performance issues. A bottleneck is the slowest part of the system. No matter how quick everything else is, you won’t be able to go faster than your bottleneck.
When planning an architecture for a new system or feature, you’re most likely to see performance trade-offs related to typical bottlenecks. These decisions can be hard to change later, so it’s essential to think about them early on.
Typical bottlenecks (you may have different ones):
- Inefficient queries to extremely large databases
- Calls to external systems over the internet (e.g., to AWS)
- Reading/writing to disk
- Rendering to the screen in 3D games
What does this mean? We will need to architect our system to account for these bottlenecks.
- We want to be smart about how we organize our database and what technology we use
- We want to minimize calls to external systems and disk
- We want to separate rendering from calculation in different threads (game engines typically handle this for you)
Forms of caching can help you deal with some bottlenecks, but you must think of what will happen if your cache fails. It’s dangerous to rely on caching alone.
Remember to use the use cases to drive these decisions so you don’t waste time deciding things that don’t matter and can focus on solving what the customer needs. (If we won’t store much data and access it away from the customer’s view, the database we choose might not be very relevant.)
Don’t worry as much during the coding phase
You might be wondering, “a lot of the advice you’ve given related to readability or extensibility will sacrifice performance like splitting code into multiple functions or using polymorphism to gain extensibility.” These days, computers are fast, and compilers are very smart, doing optimizations for you. Therefore, you should rarely sacrifice readability or extensibility for performance. It’s far more essential to be able to understand your code and make changes quickly and safely than worry about slight performance gains.
Worrying about these small things like polymorphic function calls is often fruitless. Often you’ll be bottlenecked by a slow algorithm or network call before needing to worry about using polymorphic functions. Before spending time pre-optimizing these small hunches, see if your code is too slow after you’ve written it.
That being said, don’t be silly. Algorithms can be important and often easy to make efficient if you use the right library. If you need to sort a massive list of objects, you should avoid using slow sorting algorithms like selection sort. It helps to know about algorithm complexity and different types of data structures that can help you make efficient code. Hashing is something that every developer must understand since it’s often a key to making efficient algorithms.
Profile in the testing phase if you need to
If you aren’t meeting the requirements you laid down in the requirements phase, now is your chance to fix it. If you determine your performance is sluggish, be sure to run a profiler to see where your performance bottlenecks are. Once you know that, you can focus on where you can make significant performance wins quickly.
The profiler will show you where you’re using your CPU. It may reveal slow algorithms or inefficient use of memory. Unity has a built-in profiler for your use in game development.
It took me less than 5 minutes to get this huge reduction! Unity Profiler and other modern profilers make it dead simple to find inefficiencies in your code. Don’t overoptimize!
You now have some guidelines on when to worry about performance. In general, remember:
- Your primary goal is to make your customers happy, so don’t waste your time.
- Bottlenecks are the key things to think about before you start coding new features.
- Prioritize readability and extensibility over performance while coding.
- Use a profiler if you find you’re not meeting your customer’s needs.