C# is an extraordinary programming language which is considered to be straightforward to start with and use, but it hides an abundance of complexity. Considered to be one of the best garbage collectors of managed software languages, it creates the illusion that a software developer can’t possibly go wrong. It’s true to some degree when compared to languages like C++, but it surely is misleading.
There are a lot of things you can do wrong in C#. Some of which are subtle and might go unnoticed. Others could be quite dangerous and damaging and can cause serious issues such as memory leaks, performance issues, deadlocks, and so on.
In this article, you’ll see ten practices to achieve best C# performance and mistakes to avoid. These could significantly boost your application’s performance in bottleneck algorithms and high-load parts of the code.
1. Avoiding Exceptions
One of the most expensive things that can happen in .NET is an exception being thrown into play. The addition of try/catch clauses doesn’t hurt performance by itself, but when an exception becomes one of the variables, matters turn very costly.
When running code with BenchmarkDotNet, if the ParseWithPossibleException benchmark states that a Parse has failed, it throws an exception. Whereas in the TryParse benchmark, we check if the value can be parsed and avoid throwing an exception. The TryParse benchmark is about 130 times faster than throwing an exception.
It’s advised that throwing exceptions shouldn’t be used as part of regular functionality.
2. Avoid LINQ in performance sensitive Algorithms
LINQ is fantastic, but not an excellent choice for bottleneck algorithms. The lazy evaluation mechanism and anonymous functions can hurt the performance of your application.
When compared to other languages, the LINQ code is considered to be much nicer without question. One simple line replaces about six lines of regular iteration code. However, it’s about 50% slower in this specific case.
It’s a fact that LINQ is slower than regular iterations. Also, performance wise, every use case needs to be tested individually, which slows down the process.Having said that, the LINQ part may be negligible to other logic in your algorithm, so it’s not mandatory to use LINQ alone.
3. Don’t use ‘long’ in 32-bit processes
‘Long Long Integer’ is a 64-bit value type, which is standard for all platforms, including 32-bit environments. So it is obvious that it will perform poorly in a 32-bit process. In a 64-bit process, under BenchmarkDotNet, you will notice that there will be no difference in the performance. However, in a 32-bit process the SumToInt32 benchmark performs significantly better. This phenomenon is emphasized when using the Interlocked helper class.
To run a for-loop on multiple threads, it is advised to use Parallels.For in a code. By using int , you will notice that the results are somewhat similar for a 32-bit process and a 64-bit process. But when using ‘long’ the case is different.
But it is well observed that Interlocked is much slower on long variables in 32-bit processes.
4. Use StringBuilder over String Concatenation for many items
In almost any .NET application, one of the biggest time wasters is string operations. You shouldn’t wait for your application’s memory or performance to be affected before you make the change to StringBuilder. If your processing only a couple of strings once or twice, then no worries. But if you’re going to be doing it regularly, shifting to StringBuilder will show you measurable differences in performance. Since strings are immutable, each concatenation like “a” + “b” creates a new string. That’s why it can sometimes be better to use StringBuilder.
As the number of items grows, the StringBuilder strategy is going to be even more effective.
5. Use String Concatenation over StringBuilder for few items
Unlike popular belief, StringBuilder is not always more effective than String Concatenation. Even though it is a costly affair, for a small amount of items, you are better off with regular concatenation. Let’s say we need to concatenate a thousand strings. This means we will be creating a thousand extra strings which will eventually be garbage, and most importantly would be time consuming. Thus using the regular method of Concatenating string would not be advisable. But if it is only a few items, then this method is most suitable for the task.
Depending on strings lengths, StringBuilder becomes more effective for more than 10-15 appends.
6. Use the same StringBuilder instance
When you use StringBuilder in high-frequency scenarios, you can optimize performance by using the same instance. String objects are immutable — each time you switch a method from the System.String class, you are creating a new string object in memory. This object requires a new allocation of space. In certain circumstances where you need to perform repeated modifications to a string, creating a new String object can be costly.
The overhead of allocating a new instance of StringBuilder should not be overlooked.
7. In bottleneck code areas, prefer ‘for’ to ‘foreach’
Nobody can deny that foreach provides a great syntax. But did you ever wonder whether it’s faster than regular for?
After running a simple test on BenchmarkDotNet, you can come to the conclusion that both are considerably very fast, yet the for loop is about 2 times faster than the foreach loop. The reason is that foreach has some overhead. To iterate over the values of the test, it first calls GetEnumerator() and then .Current and .MoveNext on each iteration. This conclusion will be true for List<T> . For an array, the results would be the same since the compiler transforms the foreach loop to a for loop. For a LinkedList the foreach loop will actually be much faster since direct access is very slow for that data structure.
8. Struct vs Class Allocation
It’s somewhat common knowledge that a struct can outperform classes, but it can be unclear when to use each. Struct can be used when all of the following are true:
- You are working on an array with many items (each item will be a struct).
- The struct size is less than or equals to 16 bytes (e.g. four integers). More than that size, classes become more effective than structs (source)
- The struct is short-lived
- The struct is immutable.
- The struct will not have to be boxed frequently.
By running simple simulations on GitHub’s BenchmarkDotNet, you will notice that the struct allocation is about 6.5 times faster than class allocation. Note that if you were to add properties or fields, the differences would minimize until finally, classes became more efficient (at about 48 bytes size mark).
9. Array Copy
In coding, one common practice is the copying of arrays. For big arrays or very frequent operations, this can be quite time consuming. The fastest way to copy an array is actually a quite hidden feature.
Using Buffer.BlockCopy is the fastest strategy of coping arrays.
One thing to note about Buffer.BlockCopy is that the length argument (ITEMS in a case) is in bytes. So when copying arrays other than bytes you will need to multiply by the size of the data structure.
10. Finalizers will hurt performance
In .NET, allocations are very cheap. However, de-allocation (garbage collection) is expensive. It’s even more expensive when your class has a finalizer. There are several reasons for this. Any class with a finalizer can’t be garbage collected in Gen 0, which is the fastest generation. It will then be promoted to at least one generation. Moreover, it will be placed in a dedicated finalizer queue, which is a dedicated thread to execute finalizers.
A Benchmark undertaken to show results of more than 100,000 item instances, will prove that there’s about high ratio in favor of classes without finalizers.
Some of the best practices in matters like these are:
Don’t use finalizers unless you have to. In case you must have a finalizer (for example, to dispose of native resources), use the Dispose Pattern. In this pattern, the Dispose method disposes of the native resources and calls GC.SuppressFinalize() in its end. This important method will tell the garbage collector there’s no need to call the finalizer.