Skip to content

Performance in C# — Complete Guide

DodaTech Updated 2026-06-28 6 min read

In this tutorial, you will learn about Performance in C#. We cover key concepts, practical examples, and best practices to help you master this topic.

Hook

Performance matters. Users expect fast applications, and cloud costs depend on efficient code. C# offers powerful tools for measuring and improving performance, from micro-benchmarks to large-scale profiling. Understanding performance lets you write code that is not just correct, but efficient.

Learning Path

graph LR
  A[Performance] --> B[BenchmarkDotNet]
  A --> C[Profiling]
  B --> D[Memory Analysis]
  B --> E[Span]
  C --> F[Caching]
  style A fill:#4a90d9,color:#fff
  style B fill:#4a90d9,color:#fff
  style C fill:#4a90d9,color:#fff
  style D fill:#4a90d9,color:#fff
  style E fill:#4a90d9,color:#fff
  style F fill:#4a90d9,color:#fff

BenchmarkDotNet

BenchmarkDotNet is the gold standard for micro-benchmarking in .NET.

// Install: dotnet add package BenchmarkDotNet

using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

[MemoryDiagnoser]
[RankColumn]
public class StringBenchmarks
{
    private string[] _data = null!;

    [GlobalSetup]
    public void Setup()
    {
        _data = Enumerable.Range(0, 1000)
            .Select(i => $"Item_{i}")
            .ToArray();
    }

    [Benchmark(Baseline = true)]
    public string StringBuilder()
    {
        var sb = new StringBuilder();
        foreach (var item in _data)
            sb.Append(item).Append(',');
        return sb.ToString();
    }

    [Benchmark]
    public string LinqJoin()
    {
        return string.Join(",", _data);
    }

    [Benchmark]
    public string Concat()
    {
        var result = "";
        foreach (var item in _data)
            result += item + ",";
        return result;
    }
}

// Run benchmarks
// var summary = BenchmarkRunner.Run<StringBenchmarks>();

Memory and Allocation Analysis

Use [MemoryDiagnoser] to track allocations and Garbage Collection.

| Method         | Mean     | Error   | Gen0    | Allocated |
|--------------- |---------:|--------:|--------:|----------:|
| StringBuilder  | 8.542 us | 0.123 us |  0.2136 |   7.33 KB |
| LinqJoin       | 3.124 us | 0.045 us |  0.2899 |   8.88 KB |
| Concat         | 62.45 us | 0.891 us | 2.5000 |  78.13 KB |

Span for Zero-Allocation Operations

Span<T> enables slicing and processing without allocations.

[MemoryDiagnoser]
public class SpanBenchmarks
{
    private string _dateString = "2026-06-28";

    [Benchmark(Baseline = true)]
    public (int, int, int) Substring()
    {
        var year = int.Parse(_dateString.Substring(0, 4));
        var month = int.Parse(_dateString.Substring(5, 2));
        var day = int.Parse(_dateString.Substring(8, 2));
        return (year, month, day);
    }

    [Benchmark]
    public (int, int, int) SpanSlice()
    {
        ReadOnlySpan<char> span = _dateString;
        var year = int.Parse(span.Slice(0, 4));
        var month = int.Parse(span.Slice(5, 2));
        var day = int.Parse(span.Slice(8, 2));
        return (year, month, day);
    }
}

Caching Strategies

Caching avoids redundant work and reduces latency.

public class WeatherService
{
    private readonly IMemoryCache _cache;
    private readonly IWeatherApi _api;
    private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(5);

    public WeatherService(IMemoryCache cache, IWeatherApi api)
    {
        _cache = cache;
        _api = api;
    }

    public async Task<WeatherData> GetForecastAsync(string city)
    {
        var cacheKey = $"forecast_{city}";

        if (_cache.TryGetValue(cacheKey, out WeatherData? cached))
            return cached!;

        var forecast = await _api.GetForecastAsync(city);

        _cache.Set(cacheKey, forecast, new MemoryCacheEntryOptions
        {
            AbsoluteExpirationRelativeToNow = CacheDuration,
            SlidingExpiration = TimeSpan.FromMinutes(1),
            Priority = CacheItemPriority.Normal
        });

        return forecast;
    }
}

Async Performance

Proper async usage prevents thread pool starvation.

[MemoryDiagnoser]
public class AsyncBenchmarks
{
    private readonly HttpClient _client = new();

    [Benchmark]
    public async Task<int> BlockingCall()
    {
        // BAD: blocks thread
        var response = _client.GetStringAsync("https://example.com")
            .GetAwaiter().GetResult();
        return response.Length;
    }

    [Benchmark]
    public async Task<int> AsyncCall()
    {
        // GOOD: non-blocking
        var response = await _client.GetStringAsync("https://example.com");
        return response.Length;
    }

    [Benchmark]
    public async Task<int> ConfigureAwaitCall()
    {
        var response = await _client.GetStringAsync("https://example.com")
            .ConfigureAwait(false);
        return response.Length;
    }
}

Profiling Applications

Use dotnet counters and tracing for production profiling.

# Monitor CPU and memory in real time
dotnet counters monitor --process-id 1234

# Collect trace
dotnet trace collect --process-id 1234 --providers Microsoft-Windows-DotNETRuntime

# Analyze trace
dotnet trace report trace.nettrace --analyze

Common Performance Anti-patterns

// Bad: Repeated allocation in loops
for (int i = 0; i < 1000; i++)
{
    var list = new List<int>(); // Allocates 1000 times
    list.Add(i);
}

// Good: Single allocation outside loop
var list = new List<int>(1000);
for (int i = 0; i < 1000; i++)
{
    list.Add(i);
}

// Bad: Boxing value types
ArrayList list = new ArrayList(); // ArrayList stores objects
list.Add(42); // Boxes the int

// Good: Use generics
List<int> list = new List<int>();
list.Add(42); // No boxing

// Bad: Large object heap fragmentation
byte[][] arrays = new byte[100][];
for (int i = 0; i < 100; i++)
    arrays[i] = new byte[85000]; // LOH objects

// Good: Array pooling
byte[] buffer = ArrayPool<byte>.Shared.Rent(85000);
ArrayPool<byte>.Shared.Return(buffer);

Common Mistakes

  1. Optimizing prematurely: Write correct code first, measure, then optimize. Without benchmarks, you are guessing.

  2. Ignoring allocations: Each allocation adds GC pressure. Use pooling, Span, and structs to reduce allocations.

  3. Blocking async code: Using .Result or .Wait() on async methods causes thread pool starvation and potential deadlocks.

  4. Not using ArrayPool: For temporary large arrays, ArrayPool<T>.Shared.Rent avoids LOH allocations.

  5. Overusing Concurrent collections: Concurrent collections have overhead. Use simple locking or immutable data structures when contention is low.

Practice Questions

  1. Write a BenchmarkDotNet comparison of Dictionary.TryGetValue vs ConcurrentDictionary.GetOrAdd for read-heavy workloads.

  2. Profile an ASP.NET Core application to find the slowest endpoint and optimize it.

  3. Implement a caching layer using IMemoryCache with sliding expiration and cache invalidation.

  4. Challenge: Optimize a CSV parser using Span<T> and ArrayPool<char> to minimize allocations.

FAQ

When should I use Span instead of arrays?

Use Span for slicing and processing existing memory buffers without allocation. It is ideal for parsing, encoding, and data transformation.

How do I identify memory leaks in .NET?

Use dotnet-dump to collect memory dumps, then analyze with dotnet-gcdump or Visual Studio Memory Analyzer.

What is the performance impact of reflection?

Reflection is 10-100x slower than direct calls. Cache MethodInfo and use compiled delegates or source generators for repeated calls.

How do I measure latency in production?

Use Application Insights, OpenTelemetry, or Prometheus with .NET metrics APIs to track request duration and throughput.

Is struct always faster than class?

Not always. Small structs (< 16 bytes) can be faster due to stack allocation. Large structs cause copying overhead. Measure with BenchmarkDotNet.

Mini Project: String Processing Benchmark

Compare different approaches for a common string processing task.

using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using System.Text;

[MemoryDiagnoser]
public class CsvProcessorBenchmarks
{
    private string _csvData = null!;
    private const int Iterations = 10000;

    [GlobalSetup]
    public void Setup()
    {
        var sb = new StringBuilder();
        for (int i = 0; i < 100; i++)
            sb.AppendLine($"{i},Item_{i},{i * 10.5m}");
        _csvData = sb.ToString();
    }

    [Benchmark(Baseline = true)]
    public int SplitAndParse()
    {
        var count = 0;
        var lines = _csvData.Split('\n', StringSplitOptions.RemoveEmptyEntries);
        foreach (var line in lines)
        {
            var parts = line.Split(',');
            if (int.Parse(parts[0]) > 50)
                count++;
        }
        return count;
    }

    [Benchmark]
    public int SpanBased()
    {
        var count = 0;
        ReadOnlySpan<char> data = _csvData;
        int start = 0;

        while (start < data.Length)
        {
            int end = data.Slice(start).IndexOf('\n');
            if (end == -1) end = data.Length - start;

            var line = data.Slice(start, end);
            int comma = line.IndexOf(',');
            if (comma > 0 && int.Parse(line.Slice(0, comma)) > 50)
                count++;

            start += end + 1;
        }
        return count;
    }

    [Benchmark]
    public int RegexBased()
    {
        return Regex.Matches(_csvData, @"^(\d+),", RegexOptions.Multiline)
            .Cast<Match>()
            .Count(m => int.Parse(m.Groups[1].Value) > 50);
    }
}

Performance optimization in C# is a skill that separates good developers from great ones. By using BenchmarkDotNet, Span, and proper caching strategies, you can build .NET applications that are both correct and exceptionally fast.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro