#begin
The last chapter of this book is about designing for performance. You might have heard before that, really performant code if often obscure and unreadable. But in this next chapter Prof. Ousterhout will shed some light on how to design for performance. Up until now we have discussed many strategies to battle complexity with design but nothing about performance, so here we go.
Designing for Performance
Let’s first start with Prof. Ousterhout’s thoughts on performance. He says that if you try to optimize everything for maximum speed during the development process you will create a lot of unnecessary complexity and furthermore, most “optimizations” don’t even increase performance as much as you think. But on the other hand, if you ignore performance long enough, you will eventually run into issues where your code is simply to slow. So John’s opinion is somewhere in the middle, where you use basic knowledge of performance to choose design alternatives that are naturally efficient but also clean and simple. He says that the key is to “develop an awareness of which operations are fundamentally expensive”.
Examples that come to mind here are HTTP requests, or ANY I/O operations. In a unity context, things that are slow are lots of FindObjectsOfType<T> calls and massive update loops.
Prof. Ousterhout says that the best way to find out what operations are expensive is to test them. But let me tell you that, doing performance tests, in isolation can be really difficult to pull off. However they will still give you a rough estimate of the performance of something. So I agree that performance testing is most certainly something you must do when performance is a critical aspect of your game. Which it most likely is. Also remember that, in gaming, it’s not just code that slows you down. It’s also polycount, shaders, post processing effects, lighting and shadows. So make sure you also configure your quality settings in Unity3D well, bake shadows, add occlusion culling and other best practices for increasing performance in games. I would really like to spend an blog on these kinds of things so let’s do that in the future. Let me know if that’s of interest to you!
Prof. Ousterhout explains how he did performance testing in one of his projects called RAMCloud where they created fine grained performance tests for the custom made framework they created. This is really cool and a great idea. Go check out the book if you are interested in the details.
The next section is about something very important and that is; Measure before modifying. It can be such a waste to spend a lot of time on something not worth optimizing. Trust me, I fell into this trap before. Always try to remember this because it will save you a lot of wasted time on something you thought to be slow but after a while you find out there was something else far worse in the code base.
Prof. Ousterhout says that programmers’ intuition about performance is unreliable. It’s even true for experienced developers. You always need to measure before you start optimizing. This way you know what parts of the code are actually slow. The Unity Profiler will be of great help here. When you enable “deep profiling” you are able to view some very detailed information about everything going on in your game. You can also additionally install a package called the memory profiler which allows you to create snapshots of your running game and then inspect, in great detail, all the allocated memory. This can be a real life saver to find and eradicate memory leaks! I’ve ran into the issue so many times that audioclips, sprites and/or textures were not properly destroyed. This will, at some point crash your app, especially on iOS.
Measuring the performance of your game regularly will also allow you to setup certain baseline values where you can base future improvements on. If you do not measure regularly, you might find out at some point that the performance of your have has drastically decreased. Plus, having some baseline will allow you to verify that your improvements are actually having any effect.
John then says something interesting and I’ll quote: “There’s no point in retaining complexity unless it provides a significant speedup”. I agree completely. Sometimes, in order to gain a significant performance boost, you need to accept some additional complexity like if you have to do image processing. You can use raw pointers and memory to manipulate images, or when you have to do reflection, you can instead use a FastClassGenerator. The FastClassGenerator pattern is one I first saw in a course I did on Udemy a while ago. It simply caches the calls and results to the Activator class and then, when a similar call comes in it calls the cached function to return the correct object. I checked if I could find the source code, but it’s hidden behind the paywall of Udemy so I’m not going to share it since it’s not mine to share. But it’s a course called C# Performance Tricks: How To Radically Speed Up Your Code. I wrote a blog about it so you can search it on this website and find it. It’s a great course if you want to know a bit more about increasing performance in C#.
The next section about how to design for performance is to design around the critical path of your game. So make the mission critical code fast, and do not start optimizing code that does not follow that path before you’re satisfied with the critical path performance. This seems a bit like common sense doesn’t it. I mean, you will absolutely want to optimize the code that is fundamental to your game instead of some one-off functionality you will only use in a single scene. So for example, if you’re developing an RTS game. You would want to optimize the moving and animation of all the units on the screen, the bullets and rockets flying all over the screen, and the particle effects and audio players. And you might want to include optimizations to camera movement occlusion culling as well. Don’t start optimizing the waterfall particle system, you only use in 1 scene, at a single location in the map. That will not benefit you as much as optimizing critical path functionality.
Some advise I will add to this section is that you will always want to use the correct collection types for the collections you need. Don’t just use Lists<T> for everything because it is easy. There are far better performant collection types for specific purposes, like queues, stacks, dictionaries and hashsets. Also, read up on the garbage collector that is implemented in DotNet. This is a generational garbage collector which means there is a distinction between short-and-long lived objects. If you keep objects, you will only use shortly in the short lived cycle of the garbage collector it will greatly optimize your game. Plus if you use linq you really need to watch the ToArray, ToList and ToDictionary calles that might be spread across your code because that might be a huge hit in performance when you are allocating all these copies. But alright, if you guys are interested in an blog about increasing performance in Unity we can do a full deep dive in a future blog.
But, Prof. Ousterhout ends this chapter with an example, which I’m not getting into since it’s a very detailed description and its all in the book so you can read it in there.
#end
01010010 01110101 01100010 01100101 01101110
Recent Comments