Profiling C# Code: Finding Bottlenecks in Practice
\nPerformance optimization is one of the key tasks for a developer. Even the most beautiful code can run slowly if it has bottlenecks. In this article, we will look at how to profile C# code in the .NET environment, find problem areas, and eliminate them. You will learn about practical tools such as BenchmarkDotNet, dotMemory, and the built-in Stopwatch, and also get ready-made examples for analysis.
Why is profiling important?
\nProfiling is not just about running an application and measuring time. It is a systematic approach to identifying "bottlenecks": slow algorithms, memory leaks, suboptimal database queries, or frequent heap allocations. Without profiling, you risk spending hours optimizing parts of the code that are already fast, while overlooking the real problems.
\n\n1. Profiling with Stopwatch (Quick Start)
\nThe simplest way to measure code execution time is to use the System.Diagnostics.Stopwatch class. It is suitable for a preliminary assessment, but not for precise benchmarks due to the influence of JIT compilation and background processes.
Example of using Stopwatch
\nusing System.Diagnostics;\n\npublic class Example\n{\n public void Run()\n {\n Stopwatch sw = Stopwatch.StartNew();\n\n // Code to measure\n SimulateWork();\n\n sw.Stop();\n Console.WriteLine($"Execution time: {sw.ElapsedMilliseconds} ms");\n }\n\n private void SimulateWork()\n {\n for (int i = 0; i < 1_000_000; i++)\n {\n // Simulating work\n }\n }\n}\nDrawbacks: Stopwatch does not account for JIT warm-up, garbage collection, or other factors. For serious analysis, use specialized libraries.
\n\n2. Professional Profiling with BenchmarkDotNet
\nBenchmarkDotNet is the gold standard for benchmarking in .NET. It automatically warms up the code, runs multiple iterations, analyzes statistics, and accounts for the impact of the garbage collector. Install the package via NuGet:
dotnet add package BenchmarkDotNet\n\nExample of a benchmark comparing two methods
\nusing BenchmarkDotNet.Attributes;\nusing BenchmarkDotNet.Running;\n\npublic class StringBenchmark\n{\n private string[] data = { "apple", "banana", "cherry", "date" };\n\n [Benchmark]\n public string ConcatWithPlus()\n {\n string result = "";\n foreach (var s in data)\n result += s;\n return result;\n }\n\n [Benchmark]\n public string ConcatWithStringBuilder()\n {\n var sb = new System.Text.StringBuilder();\n foreach (var s in data)\n sb.Append(s);\n return sb.ToString();\n }\n}\n\npublic class Program\n{\n public static void Main()\n {\n var summary = BenchmarkRunner.Run<StringBenchmark>();\n }\n}\nAfter running, you will see a table with time, memory allocations, and errors. This allows you to objectively compare StringBuilder and concatenation using +.
3. Memory Analysis with dotMemory and Visual Studio Diagnostic Tools
\nBottlenecks can occur not only in speed but also in memory consumption. The dotMemory tool from JetBrains and the built-in Visual Studio tools (Diagnostic Tools) help find leaks, frequent allocations, and objects that live long on the heap.
How to use Visual Studio Diagnostic Tools
\n- \n
- Run the application in Debug mode. \n
- Open the Diagnostic Tools window (Debug → Windows → Show Diagnostic Tools). \n
- Enable memory profiling (Memory Usage) or CPU profiling (CPU Usage). \n
- Take a memory snapshot (Snapshot) before and after executing the code.