Performance in C# — Complete Guide
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
Optimizing prematurely: Write correct code first, measure, then optimize. Without benchmarks, you are guessing.
Ignoring allocations: Each allocation adds GC pressure. Use pooling, Span
, and structs to reduce allocations. Blocking async code: Using
.Resultor.Wait()on async methods causes thread pool starvation and potential deadlocks.Not using ArrayPool: For temporary large arrays,
ArrayPool<T>.Shared.Rentavoids LOH allocations.Overusing Concurrent collections: Concurrent collections have overhead. Use simple locking or immutable data structures when contention is low.
Practice Questions
Write a BenchmarkDotNet comparison of
Dictionary.TryGetValuevsConcurrentDictionary.GetOrAddfor read-heavy workloads.Profile an ASP.NET Core application to find the slowest endpoint and optimize it.
Implement a caching layer using
IMemoryCachewith sliding expiration and cache invalidation.Challenge: Optimize a CSV parser using
Span<T>andArrayPool<char>to minimize allocations.
FAQ
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
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro