In part 3 I explained some common manual compiler optimization tricks. This time we go back to the code itself to find places where optimizations does a difference.
Boxing and unboxing
Boxing is the process of encapsulating a value type into a reference type. Unboxing is the reverse process. They both take some time and might happen several times in your application without your knowledge. Here is some example code on boxing/unboxing:
Since int is a primitive and thus a value type, it gets boxed and stored on the heap (where reference types live, compared to stack where value types live). This makes our lifes easy, but it also comes at a performance penalty.
You can easily identify boxing and unboxing operations by their IL code names:
Boxing is called box and unboxing is called unbox in IL code. You can use Reflector to have a look at the IL code generated by your application.
Boxing is very expensive performance wise. Try to avoid encapsulating value types into reference types. Try to find other ways of doing the same, you can use generics as an example.
Exceptions are great for reporting problems. Many logging systems catch exceptions thrown and log them to a file. The problem is that exceptions are expensive to use and catch. In high performance applications they should not be used in performance critical parts of the code. All data should be sanitized, checked and exceptions should be thrown before the data enters the performance critical part of the code.
A few things you should think about when using exceptions:
- Do not throw exceptions inside loops
- Do not throw exceptions inside performance critical code
- Do throw exceptions in helpers, managers and data layers and other run-once and use-multiple times code.
The last item can be a bit of a gray area since you can use helpers, managers and data layers inside performance critical code. I trust you to figure that part out yourself.
Strings can be your best friend or worst nightmare. They are easy to use and easy to manipulate. What could be wrong with using strings?
They are immutable. Memory allocations and thus garbage collections can become a problem. C# does a really great job of optimizing strings in such a way that minimum memory allocations take place. But even then there are still quite an overhead of using strings. Because they are immutable, all string operations create a new string instead of manipulating the old one. The new string take up some space (2 bytes for each character) and the whole memory copying operation also take some time. Lets take a look at some string concatenations:
Using this code you will get the string “0123456789”. Problem with this code is that you allocate 11 strings. Yes, that is right, 11 strings:
- initial allocation = 1
- appending = 10
- final result = 1
The allocation, the representation and writing the string to the console in C# takes up nearly 4 kb. The string operation itself takes up a couple of 100 bytes.
A better solution compared to normal strings is to this is to use the StringBuilder object. it contains a mutable (changeable) array.
The StringBuilder object allocates a lot less, but it still has an overhead. It is an object, it contains a resizable array, it has some a lot of functionality. Allocating a StringBuilder object often can actually be slower than normal strings.
Char arrays can be even faster. They are mutable and they contain no overhead.
It can be up to 10 times faster than a StringBuilder object (depending on how it is used) but it also has some disadvantages:
- You have to know the maximum size of the string (char array is fixed size)
- Can only be used in simple manipulation scenarios.