If you’ve been a developer for a few years, you may (or may not) have ever run into the need to optimize code. When should you optimize? How should you optimize? There’s an entire science (and art) called PSR: Performance, Scalability and Reliability. Since I ran into major performance issues in Valence (now fixed), I decided to share at least the basic foundations of optimization and PSR in the form of some hard-and-fast “rules” of optimization and PSR.
Rule #1: Don’t Optimize
The first rule of optimization is don’t do it. This applies double if you don’t know what you’re doing; many well-intentioned optimizations may actually decrease performance of your application, not to mention readability of your code. Premature optimization is a well-known root cause of problems.
So don’t optimize; just relax, code naturally, and focus on your design and readability. These are far more important initially.
Rule #2: Find and Quantify Problems
The second rule is to wait until a problem emerges before you try to optimize. This helps you articulate the problem, as well as the solution.
For example, you might have a query to generate a report that runs in 45 seconds (!), and should run in under ten seconds. That’s a great problem to solve.
Notice that we quantified our problem (and the solution) in this case; this is called “KPI” or “key performance indicators.” It’s important to know what you’re optimizing and how it’s supposed to be optimal.
Rule #3: Microsoft > You
The third rule is that Microsoft writes better compiler optimizations than you do. Gone are the days of unrolling for-loops, and iterating over arrays as pointers; the .NET compiler is very, very sharp, and the double compilation (once at compile-time, and again at runtime) handles a lot of the workload. In fact, I recall reading that the default optimizations can be very close to hand-optimized code. So don’t worry about it.
So now, we’ve found a problem in our application, we know what it is, and how we want to fix it. What’s the next step?
Rule #4: Measure: Baseline and Quantify
We know the problem; the next step is to measure where we’re at. In our report example, we know the report is generating in 45 seconds. Sometimes, you may not know the problem; you have to first measure and quantify it.
In Valence, I had a case where placing an atom on a heavily-filled board froze the game for around 20 seconds. By digging in, I was able to find one key algorithm that was slow (counting permutations of atoms); but by measuring, I was able to determine which of the six complex operations was the bottleneck.
The algorithm for detecting molecules works in steps:
- Calculate all possible combinations of (2, 3, … 9) atoms that might form a molecule
- Find ones with a net electronegativity of zero (balanced molecules)
- Verify that they’re all connected (no gaps of atoms
- Sort by electronegativity
DateTime.Nows between each operation, I was able to determine that the running time on a ~6 second operation were:
Which brings us to the next rule:
Rule #5: Biggest Gains First
Now, you might say “trim that second-last 0.2s to 0.1s.” True, I could do that; but I would only save 0.1s. Instead, if I even reduced the combination calculation by 25%, that would save me well over one second. So Rule #5 is: tackle the biggest gains first. Don’t optimize and streamline something trivial if it’s hardly worth the time to do so; because remember, you’re most likely trading off code quality and maintainability for high performance.
And that’s it! You should have enough of a background now to trouble-shoot and fix performance issues should they arise.
And Finally: Optimization is Creative
Performance optimization is a creative process, just like development. You might have potentially different ways to fix an issue; it’s up to you to use your creativity, experience, and judgment to pick the best one.
I recently had a performance issue in Valence, as well as high memory consumption (out of memory exceptions flying left and right). Surprisingly, the solution to both issues was to empty out un-needed collections (of permutations of atoms); this virtually solved both problems. So here are some “tricks” or techniques you can think about when optimizing:
- Don’t Trivialize Memory: Low memory can cause a lot of issues; if the CLR is thrashing (GC, collect a few bytes, add something, GC, collect a few bytes, …) that can cause performance issues too. So when trying to optimize for memory consumption, look for collections you can empty and reuse, and collections of data that might be reusable.
- Cut Down on the Operation: If you have a long operation, like a tree or graph traversal, see if you can prune the space you need to operate over; that can drastically cut down your runtime.
- Denormalize Data and Structures: Denormalizing data in databases (duplicating data so you can access it without complex joins) is pretty common; you may even have denormalized classes and structs purely to make certain operations faster — if it makes sense.
- Distract the User: Sometimes, you can’t fix the problem. In this case, you may be able to turn the user’s attention elsewhere while an operation completes. This seems to be pretty common in the gaming industry, where the use of short animations and sounds can mask a delay for an operation.
What other techniques do you use when optimizing performance?