Introduction
In this article, I will talk about some performance tips and tricks in c# to improve code performance.
Waiting synchronously on asynchronous code
Don’t wait synchronously for non-completed tasks. Like Task.Result, Task.Wait
, Task.WaitAll,
Task.WaitAny
. Any synchronous dependency between two thread pool threads is susceptible to cause thread pool starvation.
Don't use Async void
Don't use async void. An exception thrown in an async void
method is propagated to the synchronization context and might end up crashing the application. If you can’t return a task in your method move the async code to another method and call it from there
Example
interface IMyInterface
{
void DoSomething();
}
class Implementation : IMyInterface
{
public void DoSomething()
{
// This method can't return a Task,
// delegate the async code to another method
_ = DoSomethingAsync();
}
private async Task DoSomethingAsync()
{
await Task.Delay(100);
}
}
Avoid async when possible
Example
public async Task CallAsync()
{
var client = new Client();
return await client.GetAsync();
}
The code is semantically correct there is no problem with it but the async keyword is not needed here and can have significant overhead in hot paths. Try to remove it if it's possible
But you can't use that trick if your code is wrapped in blocks like try/catch or using.
public async Task Correct()
{
using (var client = new Client())
{
return await client.GetAsync();
}
}
public Task Incorrect()
{
using (var client = new Client())
{
return client.GetAsync();
}
}
In the incorrect version, since the task isn’t awaited inside of the using block, the client might be disposed of before the GetAsync
call completes.
Converting enums to string
Calling
Enum.ToString
in .net is very costly, as reflection is used internally for the conversion and calling a virtual method on struct causes boxing. As much as possible, this should be avoided.You can also create const string class for that like following
public enum Numbers
{
One,
Two,
Three,
Four
}
public static class Numbers
{
public const string One = "One";
public const string Two = "Two";
public const string Three = "Three";
public const string Four = "Four";
}
Enum comparisons
When using enums as flags, it may be tempting to use the
Enum.HasFlag
method:[Flags]
public enum Options
{
Option1 = 1,
Option2 = 2,
}
private Options _option;
public bool IsOption2Enabled()
{
return _option.HasFlag(Options.Option2);
}
This code causes two boxing allocations: one to convert
Options.Option2
to Enum
, and another one for the HasFlag
virtual call on a struct. This makes this code expensive. Instead, you should give up readability and use binary operators:public bool IsOption2Enabled()
{
return (_option & Options.Option2) == Options.Option2;
}
CancellationToken subscriptions are always inlined
Whenever you cancel a CancellationTokenSource
, all subscriptions will be executed inside of the current thread. This can lead to unplanned pauses or even deadlocks.
var cancelToken = new CancellationTokenSource();
cancelToken.Token.Register(() => Thread.Sleep(5000));
cancelToken.Cancel(); // This call will block during 5 seconds
Task.Run / Task.Factory.StartNew
If you don't have a reason to use Task.Factory.StartNew
, always use Task.Run
to start a background task. Task.Run
uses safer defaults, and more importantly, it automatically unwraps the returned task, which can prevent subtle errors with async methods.
Example
class Program
{
public static async Task ProcessAsync()
{
await Task.Delay(2000);
Console.WriteLine("Processing done");
}
static async Task Main(string[] args)
{
await Task.Factory.StartNew(ProcessAsync);
Console.WriteLine("End of program");
Console.ReadLine();
}
}
Despite the appearances, “End of program” will be displayed before “Processing done”. This is because Task.Factory.StartNew
is going to return a Task<Task>
, and the code only waits on the outer task. Correct code would be either await
or
Task.Factory.StartNew(ProcessAsync).Unwrap()await Task.Run(ProcessAsync)
.
There are only three legitimate use-cases for Task.Factory.StartNew
:
- Starting a task on a different scheduler
- Executing the task on a dedicated thread (using
TaskCreationOptions.LongRunning
) - Queuing the task on the thread pool global queue (using
TaskCreationOptions.PreferFairness
)
Memory locality matters
Let’s assume we have an array of arrays. Effectively it’s a table, 3000×3000 in size. We want to count how many slots have a value greater than zero in them.
Question – which of these two is faster?
First One
for
(
int
i =
0
; i < _map.Length; i++)
{
for
(
int
n =
0
; n < _map.Length; n++)
{
if
(_map
[i][n]
>
0
)
{
result++;
}
}
}
Second One
Answer? The first one. How much so? In my tests, I got about an 8x performance improvement on this loop!for
(
int
i =
0
; i < _map.Length; i++)
{
for
(
int
n =
0
; n < _map.Length; n++)
{
if
(_map
[n][i]
>
0
)
{
result++;
}
}
}
Notice the difference? It’s the order that we’re walking this array of arrays ([i][n] vs. [n][i]). Memory locality does indeed matter in .NET even though we’re well abstracted from managing memory ourselves.
n my case, this method was being called millions of times (hundreds of millions of times to be exact) and therefore any performance I could squeeze out of this resulted in a sizeable win. Again, thanks to my ever-handy profiler for making sure I was focused on the right place!
Relieve the pressure on the garbage collector
C#/.NET features garbage collection. Garbage collection is the process that determines which objects are currently obsolete and removing them to free space in memory. What that means is that in C#, unlike in languages like C++, you don’t have to manually take care of the removal of objects that are no longer useful, in order to claim their space in memory. Instead, the garbage collector (GC) handles all of that, so you don’t have to.
The problem is that there’s no free lunch. The collection process itself causes a performance penalty, so you don’t really want the GC to collect all the time. So how do you avoid that?
There are many useful techniques to avoid putting too much pressure on the GC. Here, I’ll focus on a single tip: avoid unnecessary allocations. What that means is to avoid things like this:
The first line creates an instance of the list that’s completely useless since the very next line returns another instance and assign its reference to the variable. Now imagine the two lines above are inside a loop that executes thousands of times? The code above might look like a silly example, but I’ve seen code like this in production—and not just a single time. Don’t focus on the example itself but on the general advice. Don’t create objects unless they’re really needed. Due to the way the GC works in .NET (it’s a generational GC process), newer objects are more likely to be collected than old ones. That means that the creation of many new, short-lived objects might trigger the GC to run.List<Product> products =
new
List<Product>();
products = productRepo.All();
Always use Stringbuilder for String concatenation operations
This point is very crucial for developers. Please use StringBuilder in place of String when you are making heavy string concatenation operations. To demonstrate how it impacts on code performance I have prepared the following example code. I am doing a string concatenation operation 500 times within the for loop.
public classTest
{
public staticstring Name { get;set; }
public staticString surname;
}
class Program
{
static void Main(string[] args)
{
string First = "A";
StringBuilder sb = new StringBuilder("A");
Stopwatch st = new Stopwatch();
st.Start();
for (int i = 0; i < 500; i++)
{
First = First + "A";
}
st.Stop();
Console.WriteLine("Using String :-" + st.ElapsedTicks);
st.Restart();
for (int i = 0; i < 500; i++)
{
sb.Append("A");
}
st.Stop();
Console.WriteLine("Using Stringbuilder :-" + st.ElapsedTicks);
Console.ReadLine(); } }
// Using String : -463
// Using StringBuilder: -24
Post Comments