Profiling C# Code: Finding Bottlenecks and Speeding Up Applications

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Introduction: Why Profiling Matters

Every C# developer has encountered a situation where an application runs slowly, but it's unclear why. Intuitive guesses are often wrong: code that seems slow may be fast, and "obvious" optimizations may be useless. This is where profiling comes to the rescue — a systematic analysis of code performance to identify bottlenecks.

Profiling in .NET is not just about measuring execution time. It's a whole set of techniques: from a simple Stopwatch to advanced tools like BenchmarkDotNet and PerfView. Without profiling, you risk optimizing the wrong things, spending hours on useless changes.

In this article, we will break down practical methods for profiling C# code, learn to find real bottlenecks, and understand which tools to use in each specific case.



1. Basic Profiling with Stopwatch

The simplest way to measure execution time is to use the System.Diagnostics.Stopwatch class. It is suitable for quick checks and simple scenarios, but has limitations: high overhead (nanoseconds) and lack of statistics.

using System.Diagnostics;

public class SimpleProfiler{ public static void Measure() { var sw = Stopwatch.StartNew(); // Code under test var list = Enumerable.Range(0, 10_000_000).ToList(); sw.Stop(); Console.WriteLine($"Time: {sw.ElapsedMilliseconds} ms"); }}

When to use: for rough estimation, when you need a quick answer without installing additional packages. Not suitable for precise measurements (e.g., when comparing algorithms).

Disadvantages: a single measurement can be random (GC, background processes). You need to run it multiple times and average the results — this is already done by BenchmarkDotNet.



2. BenchmarkDotNet: The Gold Standard for Benchmarks

BenchmarkDotNet is a library for creating accurate and reproducible benchmarks. It automatically warms up the JIT, avoids GC influence, and provides detailed statistics.

Installation and Basic Example

// Installation: dotnet add package BenchmarkDotNetusing BenchmarkDotNet.Attributes;using BenchmarkDotNet.Running;

public class StringBenchmark{ private string _text = "Hello, World!"; [Benchmark] public bool ContainsWithOrdinal() => _text.Contains("World", StringComparison.Ordinal); [Benchmark] public bool ContainsWithInvariant() => _text.Contains("World", StringComparison.InvariantCulture);}

class Program{ static void Main() => BenchmarkRunner.Run<StringBenchmark>();}

The result will show that Ordinal is 2-3 times faster. Without a benchmark, you wouldn't have noticed this.

Advanced Features

  • Params: test with different data sizes.
  • MemoryDiagnoser: shows memory allocations.
  • Baseline: compare performance against a reference.
[MemoryDiagnoser]public class ListBenchmark{    [Params(100, 1000, 10000)]    public int N;        private List<int> _list;        [GlobalSetup]    public void Setup() => _list = Enumerable.Range(0, N).ToList();        [Benchmark(Baseline = true)]    public int ForLoop()    {        int sum = 0;        for (int i = 0; i < _list.Count; i++) sum += _list[i];        return sum;    }        [Benchmark]    public int ForeachLoop()    {        int sum = 0;        foreach (var item in _list) sum += item;        return sum;    }}


3. Memory and GC Profiling

Memory leaks and frequent garbage collections are a common cause of slowdowns in .NET. For analysis, use:

3.1. Memory Profiler (JetBrains dotMemory)

Blogs

Book Recommendations