In part 2 I gave a list of the different C# keywords that could potentially increase the performance of your code. This time we take a look at the different manual inlining techniques – it will not be like the two previous posts as this one can’t be presented in simple code snippets. The compiler is normally in charge of doing those kinds of optimizations, but sometimes it might not because of some requirements. First off, let’s get an overview of the different optimizations done by the C# compiler:
- Constant folding
- Constant and copy propagation
- Common subexpression elimination
- Code motion of loop invariants
- Dead store and dead code elimination
- Register allocation
- Method inlining
- Loop unrolling (small loops with small bodies)
As you see, it is quite effective and contains the mostly used optimization techniques required by modern compilers.
Why manually optimize?
Well, the problem is that while the compiler almost always takes the right path, there are times where manual inlining is necessary. One example is when you are using an old .net version like 2.0; It will not inline methods that contain a value type parameter.
Another example is automatic properties. They will get inlined in release mode and all that, but they are considered regular methods by the compiler and thus still fall under the requirements of the compiler (see them further down).
The compiler has a few requirements to a method before it can be inlined:
- Methods that are greater than 32 bytes of IL will not be inlined.
- Virtual functions are not inlined.
- Methods that have complex flow control will not be in-lined. Complex flow control is any flow control other than
if/then/else;in this case,
- Methods that contain exception-handling blocks are not inlined, though methods that throw exceptions are still candidates for inlining.
- If any of the method's formal arguments are structs, the method will not be inlined.
While all of them are great requirements and suffice to say that the compiler is really clever in this area (it determines inlining depending on cache size etc.) a few of those items can pose as limitations in your application. More precisely: Virtual functions, struct arguments and exception handling. I’m going to take each of them separately.
The only way to get around this one is to simply not use virtual calls. If you have a way of getting rid of virtual methods, do it. Virtual calls are up to 40% slower than static or instance method calls. If you get rid of them, the compiler might also inline more methods.
.Net 3.5 SP1 brought some improvements to this area and it now consider methods with struct arguments as candidates for inlining. However, if you use < .net 3.5 SP1, you will need to identify the methods yourself and inline then manually.
I’m not going to tell you not to do exception handling. While there are other ways of reporting errors, you might be forced to use try/catch. To get around this you need to make sure that the operation expensive methods does not contain exception handling code. Always test input once and then send it off to be calculated on.
Another thing is properties. You might have a ListCapacity property that checks if the list capacity is < 0 and throw an error if that is the case. Using that property directly in a loop can give you a performance penalty. Instead you should have a backing field (a simple variable) that contains the value and get/set from/to that instead.
As I mentioned in part 1, you should move your work outside of loops and you should not even use loops if you don’t need to. Loop unrolling is (almost) the same thing. A classic example of loop unrolling:
The C# compiler only inlines small loops with small bodies. Inlining a huge loop is very inefficient since the code size would increase and thus you will get slower code execution. But intelligently unrolling a loop can greatly increase performance, so beware of what you inline and remember to profile it.