Optimization 101

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:

  1. Calculate all possible combinations of (2, 3, … 9) atoms that might form a molecule
  2. Find ones with a net electronegativity of zero (balanced molecules)
  3. Verify that they’re all connected (no gaps of atoms
  4. Sort by electronegativity

By sticking DateTime.Nows between each operation, I was able to determine that the running time on a ~6 second operation were:

  1. 4.5s
  2. 1.1s
  3. 0.2s
  4. 0.2s

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?

About Ashiq Alibhai, PMP

Ashiq has been coding C# since 2005. A desktop, web, and RIA application developer, he's touched ASP.NET MVC, ActiveRecord, Silverlight, NUnit, and all kinds of exciting .NET technologies. He started C# City in order to accelerate his .NET learning.
This entry was posted in Core .NET, Silverlight, Uncategorized, Web, Wndows Forms and tagged , , , . Bookmark the permalink.

One Response to Optimization 101

  1. David MacKay says:

    “We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil” -Donald Knuth

    Rule #4 (Measure: Baseline and Quantify) is hugely important and hits quite close to home for me. If you don’t measure you won’t know what to fix.

    Performance problems can sneak up on you if you’re not measuring performance continuously. If most of your performance defects are coming from production systems you aren’t doing enough to identify the bottlenecks before they become customer problems.

    In the past, I’ve found that simple stopwatch (start/stop timing) performance timing of unit/integration tests to be a good early indicator of performance problems to come. If a test is slower than a previous iteration, you know exactly what component to analyze for performance issues.

    Thanks for the blog post.

Leave a Reply

Your email address will not be published. Required fields are marked *