Home C# Code Optimization Tricks
Post
Cancel

C# Code Optimization Tricks

About Optimization

This is such a thing, quite complicated in places, and before we discuss it, we need to understand a few things.

The first thing is when we’re doing optimization we know how things work, for example, details of implementation, and the specific things, also very often it’s easy to do a mistake and add an optimization trick that you found here, doesn’t check it out, and your optimization will get even worse than before, on the other hand, it’s not hard to do right.

You don’t really need to be a .NET Senior to write optimized code for your project, knowing the basics is enough, but sometimes would be useful to be a Senior to know the more tricks based on huge background experience.

Setup

BenchmarkDotNet Logo

In case when you want to test the perfomance of something I highly recommend you to use the best and most popular way in .NET is BenchmarkDotNet, also this post is based on this library, you will understand later, why. As an example why this library is so popular, even the dotnet perfomance uses this library for the benchmarking.

Very simple example of the benchmark:

1
BenchmarkRunner.Run<MathBenchmark>();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class MathBenchmark
{
    private int _a;
    private int _b;

    [GlobalSetup]
    public void Setup()
    {
        _a = 10;
        _b = 10;
    }
    
    [Benchmark]
    public int Multiply()
    {
        return _a * _b;
    }
    [Benchmark]
    public int Divide()
    {
        return _a / _b;
    }
}

After running it, I got such output:

1
2
3
4
5
BenchmarkDotNet=v0.13.4, OS=Windows 11 (10.0.22621.1105)
11th Gen Intel Core i5-1135G7 2.40GHz, 1 CPU, 8 logical and 4 physical cores
.NET SDK=7.0.100
  [Host]     : .NET 6.0.11 (6.0.1122.52304), X64 RyuJIT AVX2
  DefaultJob : .NET 6.0.11 (6.0.1122.52304), X64 RyuJIT AVX2
1
2
3
4
|   Method |      Mean |     Error |    StdDev | Median |
|--------- |----------:|----------:|----------:|-------:|
| Multiply | 0.1137 ns | 0.0581 ns | 0.1714 ns | 0.0 ns |
|   Divide | 0.1136 ns | 0.0618 ns | 0.1793 ns | 0.0 ns |

Tricks

I want to notice that these tricks are tested on .NET 6.0 - which is actually yet not bad optimized.

I’ll use such terminology for rating the tricks from easy-hard, than higher number than harder the tricks and requires knowledge of the how works compiler in depth or other complex things.

So, you after reading the post you could say - Uhm.. like, you have the Hard trick, but this trick is so easy., for example the easy - means you doesn’t need to know compiler peculiarities, etc. Than higher we are moving with the easy-hard, than more you need to know the context to understand the purpose of the trick.

  • Easy (1 +)
  • Medium (2 ++)
  • Hard (3 +++)

How to find these tricks yourself

When you used to do something many times and you know how to do that better, kinda like best practice, you probably might know how it works in depth and which way could be better to use in this context, this is how you can do that.

Easy

Let’s start, you may know these tricks, but, just remind some of them.

foreach IEnumerable vs List

1
2
list = Enumerable.Range(0, 100).ToList();
enumerable = list; // IEnumerable<int>
1
2
3
4
5
var result = 0;
foreach (var value in enumerable)
{
    result += value;
}

610.91 ns

1
2
3
4
5
var result = 0;
foreach (var value in list)
{
    result += value;
}

84.47 ns

List.Capacity

1
2
3
4
5
var list = new List<int>();
for (var i = 0; i < 100; i++)
{
    list.Add(i);
}

365.7 ns

1
2
3
4
5
6
var list = new List<int>();
list.Capacity = 100;
for (var i = 0; i < 100; i++)
{
    list.Add(i);
}

221.7 ns

Contains & IEquatable

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public struct A
{
    public int Value;
}
public struct B : IEquatable<B>
{
    public bool Equals(B other)
    {
        return other.Equals(this);
    }
}

refsA = new List<A>();
refsB = new List<B>();
1
refsB.Contains(new B());

0.6466 ns

1
refsA.Contains(new A());

0.4640 ns

Medium

NoInlining vs AggressiveInlining

1
2
3
4
5
6
7
8
[MethodImpl(MethodImplOptions.NoInlining)]
public void NoInlining()
{
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AggressiveInlining()
{
}
1
NoInlining();

0.4838 ns

1
AggressiveInlining();

0.0332 ns

List foreach vs for

1
values = Enumerable.Range(0, 100).ToList();
1
2
3
foreach (var value in values)
{
}

85.20 ns

1
2
3
for (int i = 0; i < values.Count; i++)
{
}

32.79 ns

Finalizers

1
2
3
4
5
6
7
8
9
public class A
{
}
public class B
{
   ~B()
   {
   }
}
1
new B();

130.9663 ns

1
new A();

0.0349 ns

ArrayPool

1
2
text = "Hello";
lenght = text.Length;
1
2
3
4
5
var buffer = new byte[256];
var bytesCount = Encoding.UTF8.GetBytes(text, 0, lenght, buffer, 0);
using var memoryStream = new MemoryStream();
memoryStream.Write(buffer, 0, lenght);
var bytes = memoryStream.ToArray();

92.81 ns (allocated 656 B)

1
2
3
4
5
6
7
var buffer = ArrayPool<byte>.Shared
    .Rent(256);
var bytesCount = Encoding.UTF8.GetBytes(text, 0, lenght, buffer, 0);
using var memoryStream = new MemoryStream();
memoryStream.Write(buffer, 0, lenght);
ArrayPool<byte>.Shared.Return(buffer);
var bytes = memoryStream.ToArray();

108.10 ns (allocated 376 B)

stackalloc

1
2
3
4
5
var buffer = new byte[256];
var bytesCount = Encoding.UTF8.GetBytes(text, 0, lenght, buf
using var memoryStream = new MemoryStream();
memoryStream.Write(buffer, 0, lenght);
var bytes = memoryStream.ToArray();

96.92 ns (allocated 656 B)

1
2
3
4
5
Span<byte> buffer = stackalloc byte[256];
var bytesCount = Encoding.UTF8.GetBytes(text, buffer);
using var memoryStream = new MemoryStream();
memoryStream.Write(buffer.Slice(0, bytesCount));
var bytes = memoryStream.ToArray();

78.81 ns (allocated 376 B)

Hard

Delegate.CreateDelegate

1
2
3
4
5
6
7
8
9
10
11
public class API
{
    public static void StaticVoidMethod()
    {
    }
}

methodInfo = typeof(API).GetMethod(nameof(API.StaticVoidMethod));
@delegate = (Action)Delegate.CreateDelegate(typeof(Action), methodInfo);

// If you need a method with a return type use (Func<T>)Delegate.CreateDelegate((Func<T>), methodInfo)
1
methodInfo.Invoke(null, Array.Empty<object>());

39.890 ns

1
@delegate.Invoke();

1.593 ns

Unsafe.As

1
2
3
4
5
public class A
{
}

@object = new A();
1
var obj = (A)@object;

0.5847 ns

1
var obj = Unsafe.As<object, A>(ref @object);

0.2757 ns

How the thing that you use is work

  • Use online source of dotnet, you can specify there anything you wish to see.
  • Use decompilers to see the code such as dnSpyEx (DO NOT use old dnSpy because you will not be able to the async methods), ILSpy, and dotPeek, highly recommend you to use dnSpy because it’s much better. The problem is you will never see the actual “source code” using the decompiler, there’s no source code anymore if you’re using the decompiler for that because most of the things are optimized while the application is being compiled, so, most of the things can be different after decompiling it.
This post is licensed under CC BY 4.0 by the author.

Understanding the Importance of File Paths in Portable Executables (PE)

No one pays me for the Unit Tests