Introduction: Why Performance Matters from the Start?
\nEvery C# developer sooner or later encounters a situation where the application starts to slow down. Often, performance issues are introduced early on — due to suboptimal algorithms, improper memory management, or excessive use of LINQ without understanding its inner workings. For beginners, this topic may seem complex, but in reality, the basic principles of optimization are quite simple and can be mastered in a few hours of practice.
\nIn this article, we will cover four key aspects that will help you write fast and efficient C# applications on the .NET platform. You will learn how to profile code, optimize LINQ queries, manage memory correctly, and use asynchrony. The code examples will be as close as possible to real-world tasks that developers face.
\n\n1. Profiling: The First Step to Optimization
\nBefore optimizing anything, you need to understand what exactly is slowing down. Intuitive guesses are often wrong. Profiling is the process of measuring your code's performance using specialized tools.
\n\nProfiling Tools
\n- \n
- Visual Studio Diagnostic Tools — a built-in tool available in Community edition and above. It allows you to track CPU usage, memory, and execution time. \n
- JetBrains dotMemory / dotTrace — powerful commercial utilities for memory and execution time analysis. \n
- BenchmarkDotNet — a lightweight library for microbenchmarking. Ideal for comparing two implementations of the same method. \n
Example: Measuring Execution Time
\nUsing Stopwatch for a simple measurement:
using System.Diagnostics;\n\nvar stopwatch = Stopwatch.StartNew();\n// Your code\nfor (int i = 0; i < 1000000; i++)\n{\n // Some operation\n}\nstopwatch.Stop();\nConsole.WriteLine($"Execution time: {stopwatch.ElapsedMilliseconds} ms");\nTip: For accurate measurements, use BenchmarkDotNet, as Stopwatch does not account for JIT compilation overhead.
2. Memory and String Management
\nMemory management in .NET is automated (Garbage Collector), but that doesn't mean you can ignore it. One of the most common issues is excessive string creation.
\n\nProblem: String Concatenation in a Loop
\nStrings in C# are immutable. Each + operation creates a new string, leading to constant allocations and pressure on the GC:
string result = "";\nfor (int i = 0; i < 10000; i++)\n{\n result += i.ToString(); // BAD: creates 10000 new strings\n}\n\nSolution: StringBuilder
\nThe StringBuilder class works with a character buffer and minimizes allocations:
var sb = new StringBuilder();\nfor (int i = 0; i < 10000; i++)\n{\n sb.Append(i);\n}\nstring result = sb.ToString(); // GOOD: one allocation at the end\n\nAvoid Boxing
\nBoxing (wrapping value types into an object) is another source of hidden allocations. Example:
\nArrayList list = new ArrayList();\nlist.Add(42); // Boxing: int is wrapped into object\n\n// Better to use generic collections:\nList<int> list2 = new List<int>();\nlist2.Add(42); // No boxing\n\n3. Optimizing LINQ Queries
\nLINQ is a powerful tool, but improper use can significantly slow down your application. The main issue is multiple iterations over collections.
\n\nProblem: Multiple Iterations
\nEach call to Where(), Select(), Count() can iterate over the collection again if the result is not materialized:
var numbers = Enumerable.Range(1, 1000000);\nvar evenNumbers = numbers.Where(n => n % 2 == 0);\nint coun