Skip to main content

C# Performance Tips

Introduction

Performance optimization is a critical aspect of software development that can significantly impact user experience and application efficiency. In C#, understanding how to write performant code is particularly important as applications grow in complexity and scale. This guide explores practical techniques to enhance your C# applications' performance without sacrificing code readability or maintainability.

Whether you're developing web applications, desktop software, or mobile apps with Xamarin, these performance tips will help you write code that executes faster, uses less memory, and provides a better overall experience for your users.

Why Performance Matters

Even in today's era of powerful hardware, performance remains crucial for several reasons:

  1. Better user experience
  2. Lower infrastructure costs
  3. Increased application scalability
  4. Reduced energy consumption (especially important for mobile applications)
  5. Ability to handle larger datasets and more concurrent users

Core Performance Optimization Areas

1. Memory Management

Use Value Types Appropriately

Value types (struct) can help reduce memory allocations compared to reference types (class) for small, simple data structures.

csharp
// Less efficient for small data
public class Point
{
public int X { get; set; }
public int Y { get; set; }
}

// More efficient for small data
public struct PointStruct
{
public int X { get; set; }
public int Y { get; set; }
}

However, be careful when passing large structs as method parameters, as they'll be copied each time.

Implement IDisposable and Use Using Statements

Always dispose of objects that implement IDisposable to release unmanaged resources promptly.

csharp
// Good practice
public void ReadFile(string path)
{
using (var fileStream = new FileStream(path, FileMode.Open))
{
// Use the fileStream
// Automatically disposed when exiting the using block
}
}

// Alternative syntax in C# 8.0 and later
public void ReadFile(string path)
{
using var fileStream = new FileStream(path, FileMode.Open);
// Use the fileStream
// Automatically disposed when exiting the method
}

Minimize Boxing and Unboxing

Boxing (converting value types to reference types) and unboxing (converting back) add performance overhead.

csharp
// Avoid this - involves boxing
ArrayList list = new ArrayList();
list.Add(42); // Boxes the int to an object

// Better approach - no boxing
List<int> list = new List<int>();
list.Add(42); // No boxing occurs

2. String Operations

Use StringBuilder for Multiple Concatenations

String concatenation in a loop creates many temporary string objects. StringBuilder is more efficient for this scenario.

csharp
// Inefficient way
string result = "";
for (int i = 0; i < 10000; i++)
{
result += i.ToString(); // Creates a new string each iteration
}

// Efficient way
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++)
{
sb.Append(i); // No new strings created
}
string result = sb.ToString(); // Creates the final string once

Example with timing:

csharp
using System;
using System.Diagnostics;
using System.Text;

public class StringPerformanceDemo
{
public static void Main()
{
const int iterations = 10000;

Stopwatch sw = Stopwatch.StartNew();
string result1 = "";
for (int i = 0; i < iterations; i++)
{
result1 += i.ToString();
}
sw.Stop();
Console.WriteLine($"String concatenation: {sw.ElapsedMilliseconds}ms");

sw.Restart();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < iterations; i++)
{
sb.Append(i);
}
string result2 = sb.ToString();
sw.Stop();
Console.WriteLine($"StringBuilder: {sw.ElapsedMilliseconds}ms");
}
}

// Output (approximate):
// String concatenation: 350ms
// StringBuilder: 3ms

Use String.Intern for Repeated Strings

When working with many identical strings, consider using String.Intern to store only one copy in memory.

csharp
string s1 = new string(new char[] { 'h', 'e', 'l', 'l', 'o' });
string s2 = new string(new char[] { 'h', 'e', 'l', 'l', 'o' });

// s1 and s2 are different objects in memory
Console.WriteLine(Object.ReferenceEquals(s1, s2)); // False

// Intern the strings
string interned1 = String.Intern(s1);
string interned2 = String.Intern(s2);

// Now they reference the same string in memory
Console.WriteLine(Object.ReferenceEquals(interned1, interned2)); // True

3. LINQ and Collections

Be Mindful of LINQ Query Execution

LINQ provides elegant code but can introduce performance overhead. Use it judiciously.

csharp
// This query executes multiple times
var numbers = Enumerable.Range(1, 100).ToList();
for (int i = 0; i < 1000; i++)
{
var evenNumbers = numbers.Where(n => n % 2 == 0);
foreach (var num in evenNumbers)
{
// Process num
}
}

// More efficient: execute once and reuse
var numbers = Enumerable.Range(1, 100).ToList();
var evenNumbers = numbers.Where(n => n % 2 == 0).ToList(); // Materialize once
for (int i = 0; i < 1000; i++)
{
foreach (var num in evenNumbers)
{
// Process num
}
}

Choose the Right Collection Type

Select appropriate collection types based on your access patterns:

csharp
// For index-based access:
List<string> list = new List<string>();

// For key-based lookups:
Dictionary<string, Customer> customersByName = new Dictionary<string, Customer>();

// For fast lookups with no duplicates:
HashSet<string> uniqueNames = new HashSet<string>();

// For sorted data:
SortedList<int, string> sortedItems = new SortedList<int, string>();

Specify Collection Capacity When Known

Initializing collections with a known capacity prevents resizing operations:

csharp
// Without capacity (will resize as elements are added)
var list = new List<int>();

// With capacity (more efficient when size is known)
var list = new List<int>(10000);

4. Async/Await Best Practices

Avoid Blocking on Async Code

Blocking async operations defeats their purpose and can lead to deadlocks.

csharp
// Bad practice - blocks the thread
public void DoWork()
{
var result = GetDataAsync().Result; // Blocks until complete
// Process result
}

// Good practice
public async Task DoWorkAsync()
{
var result = await GetDataAsync(); // Non-blocking
// Process result
}

Use ConfigureAwait(false) in Libraries

In library code, use ConfigureAwait(false) to avoid forcing the continuation back to the original context:

csharp
// In library code
public async Task<Data> GetDataAsync()
{
// Doesn't need to resume on the original SynchronizationContext
var result = await httpClient.GetAsync(url).ConfigureAwait(false);
return await ProcessDataAsync(result).ConfigureAwait(false);
}

5. Practical Example: Optimizing a File Parser

Let's look at a real-world example of optimizing a CSV file parser:

csharp
// Original, less efficient implementation
public static List<Person> ParseCsvFile(string filePath)
{
var people = new List<Person>();
var lines = File.ReadAllLines(filePath); // Loads entire file into memory

foreach (var line in lines.Skip(1)) // Skip header
{
var parts = line.Split(',');
if (parts.Length >= 3)
{
var person = new Person
{
Name = parts[0],
Age = int.Parse(parts[1]),
Email = parts[2]
};
people.Add(person);
}
}

return people;
}

// Optimized implementation
public static async Task<List<Person>> ParseCsvFileOptimizedAsync(string filePath)
{
var people = new List<Person>();

// Process file line by line without loading everything
using (var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, true))
using (var streamReader = new StreamReader(fileStream))
{
// Skip header
await streamReader.ReadLineAsync().ConfigureAwait(false);

string line;
while ((line = await streamReader.ReadLineAsync().ConfigureAwait(false)) != null)
{
var parts = line.Split(',');
if (parts.Length >= 3)
{
var person = new Person
{
Name = parts[0],
Age = int.TryParse(parts[1], out int age) ? age : 0,
Email = parts[2]
};
people.Add(person);
}
}
}

return people;
}

public class Person
{
public string Name { get; set; }
public int Age { get; set; }
public string Email { get; set; }
}

The optimized version:

  • Processes the file stream asynchronously
  • Reads the file line by line instead of loading it entirely
  • Uses a buffer size appropriate for file operations
  • Handles parsing errors gracefully with TryParse

Performance Measurement and Profiling

Using BenchmarkDotNet

The best way to ensure your optimizations are effective is to measure performance before and after changes.

csharp
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

public class StringBenchmarks
{
private const int N = 10000;

[Benchmark]
public string ConcatenationInLoop()
{
string result = "";
for (int i = 0; i < N; i++)
{
result += i.ToString();
}
return result;
}

[Benchmark]
public string StringBuilder()
{
var sb = new System.Text.StringBuilder();
for (int i = 0; i < N; i++)
{
sb.Append(i);
}
return sb.ToString();
}

public static void Main()
{
var summary = BenchmarkRunner.Run<StringBenchmarks>();
}
}

Stopwatch for Quick Measurements

For quick performance checks, System.Diagnostics.Stopwatch provides an easy way to time operations:

csharp
using System;
using System.Diagnostics;

public class PerformanceTester
{
public static void MeasureOperation(Action operation, string name, int iterations = 1)
{
// Warm-up
operation();

var sw = Stopwatch.StartNew();

for (int i = 0; i < iterations; i++)
{
operation();
}

sw.Stop();

Console.WriteLine($"{name}: {sw.ElapsedMilliseconds}ms for {iterations} iterations " +
$"({(double)sw.ElapsedMilliseconds / iterations:F3}ms per iteration)");
}
}

// Usage
MeasureOperation(() => {
// Code to measure
var list = Enumerable.Range(0, 1000000).ToList();
}, "Create list", 10);

Summary

Performance optimization in C# is a balance between writing readable code and ensuring efficient execution. The key takeaways from this guide include:

  1. Memory management: Use value types appropriately, implement IDisposable, and minimize boxing operations.
  2. String operations: Leverage StringBuilder for multiple concatenations and consider string interning for repeated strings.
  3. Collections and LINQ: Choose the right collection type, initialize with capacity when known, and be mindful of deferred execution in LINQ.
  4. Asynchronous programming: Follow best practices with async/await and use ConfigureAwait(false) in library code.
  5. Measurement: Always measure performance before and after optimization to ensure your changes are effective.

Remember, premature optimization is the root of much programming evil. Focus on writing clean, maintainable code first, and optimize only when necessary, based on actual performance measurements rather than assumptions.

Additional Resources

Practice Exercises

  1. String Builder vs String Concatenation: Create a benchmark comparing string concatenation with StringBuilder for different numbers of operations.
  2. Collection Performance: Compare the performance of List<T>, Dictionary<K,V>, and HashSet<T> for adding and retrieving 100,000 items.
  3. LINQ Optimization: Take a complex LINQ query and optimize it by reducing redundant operations and materializing results where appropriate.
  4. Memory Profiling: Use a memory profiler (like Visual Studio's built-in tools or dotMemory) to analyze and optimize a simple application's memory usage.
  5. Parallel Processing: Implement a CPU-intensive operation both sequentially and using the Parallel class, then compare their performance on different sized inputs.


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)