← Back to Articles         
C# Performance Optimization

C# Performance Optimization

Published on 12 Oct 2026     7 min read C#
C# Performance Optimization

C# Performance Optimization – Interview Questions

1. How do you identify and improve C# application performance?

Explanation:

Performance optimization means identifying slow operations, high CPU usage, excessive memory consumption, and unnecessary database calls.

Common techniques:

  • Use profiling tools to identify bottlenecks.

  • Use async/await for I/O operations.

  • Optimize LINQ queries and SQL queries.

  • Reduce unnecessary object creation.

  • Implement caching for frequently accessed data.

Example:

// Less efficient
var result = employees
    .Where(x => x.IsActive)
    .ToList()
    .Count;

// Better
var result = employees.Count(x => x.IsActive);

Explanation: The second approach counts directly without creating an intermediate list.

Key Points:

  • Measure performance before optimizing.

  • Use Visual Studio Performance Profiler or dotnet-counters.

  • Focus on CPU, memory, database, and response time.

  • Optimize the actual bottleneck rather than guessing.

 

2. Why is StringBuilder better for repeated string concatenation?

Explanation:

In C#, string is immutable. Every concatenation creates a new string.

StringBuilder maintains a mutable buffer, reducing unnecessary allocations during repeated concatenation.

Example:

// String concatenation
string result = "";

for (int i = 0; i < 1000; i++)
{
    result += i;
}

// StringBuilder
StringBuilder sb = new StringBuilder();

for (int i = 0; i < 1000; i++)
{
    sb.Append(i);
}

string output = sb.ToString();

Diagram:

String:
"A" -> "AB" -> "ABC" -> "ABCD"
        New strings created

StringBuilder:
Buffer -> Append -> Append -> Append
          Reuses buffer

Key Points:

  • string is immutable.

  • StringBuilder is mutable.

  • Use StringBuilder for repeated concatenation, especially inside loops.

  • For a few concatenations, normal string operations are often sufficient.

 

3. How do boxing and unboxing affect performance?

Explanation:

  • Boxing: Converts a value type into an object, typically allocating memory on the heap.

  • Unboxing: Extracts a value type from a boxed object.

Frequent boxing increases memory allocations and garbage collection overhead.

Example:

int number = 100;

// Boxing
object obj = number;

// Unboxing
int result = (int)obj;

// Better: Generic collection
List<int> numbers = new List<int>();

numbers.Add(100);

Explanation:

List<int> stores integers without boxing each element, unlike ArrayList.

Key Points:

  • Boxing generally creates a heap allocation.

  • Unboxing requires type checking and copying the value.

  • Avoid unnecessary boxing in frequently executed code.

  • Prefer generic collections such as List<T>.

 

4. How can LINQ cause performance issues?

Explanation:

LINQ improves code readability, but inefficient queries can cause repeated enumeration, unnecessary allocations, or excessive database queries.

Example:

var employees = GetEmployees();

// Less efficient
var result = employees
    .Where(x => x.IsActive)
    .ToList()
    .Count;

// Better
int count = employees.Count(x => x.IsActive);

Explanation:

The first query creates a new list before counting. The second directly counts matching records.

Key Points:

  • Avoid unnecessary ToList() calls.

  • Avoid executing the same LINQ query repeatedly.

  • Use Any() instead of Count() > 0 when checking existence.

  • In EF Core, avoid N+1 database queries.

  • Use AsNoTracking() for read-only EF Core queries.

 

5. How do unnecessary allocations affect garbage collection?

Explanation:

Every unnecessary heap allocation increases memory usage and creates additional work for the Garbage Collector (GC).

Frequent allocations can trigger more GC activity and affect application performance.

Example:

// Unnecessary allocation
for (int i = 0; i < 1000; i++)
{
    int[] numbers = new int[100];
}

// Better: Reuse the array
int[] numbers = new int[100];

for (int i = 0; i < 1000; i++)
{
    numbers[0] = i;
}

Explanation:

The first approach creates 1,000 arrays. The second creates one array and reuses it.

Diagram:

More Allocations
       |
       v
More Heap Memory Usage
       |
       v
More GC Activity
       |
       v
Possible Performance Impact

Key Points:

  • Avoid creating unnecessary temporary objects.

  • Reuse buffers where appropriate.

  • Consider ArrayPool<T> for reusable temporary arrays.

  • Excessive GC activity can increase CPU usage and latency.

 

6. What is the difference between IEnumerable and IQueryable from a performance perspective?

Explanation:

IEnumerable<T>

IQueryable<T>

Supports in-memory enumeration

Supports provider-translated queries

LINQ-to-Objects filters in memory

EF Core can translate filters into SQL

May load more records if filtering occurs after materialization

Can filter records at the database

Suitable for in-memory collections

Suitable for database queries

Example:

// IEnumerable
IEnumerable<Employee> employees =
    context.Employees.ToList();

var result = employees
    .Where(x => x.IsActive);

// IQueryable
IQueryable<Employee> employeesQuery =
    context.Employees;

var result2 = employeesQuery
    .Where(x => x.IsActive)
    .ToList();

Explanation:

  • IEnumerable: All employees are loaded before filtering.

  • IQueryable: EF Core can translate the filter into SQL and retrieve only active employees.

Diagram:

IEnumerable:
Database -> All Records -> Memory Filter

IQueryable:
Database -> SQL WHERE -> Filtered Records

Key Points:

  • IQueryable supports database-side filtering with EF Core.

  • IEnumerable is appropriate for in-memory processing.

  • Both support deferred execution in many LINQ operations.

  • Avoid calling ToList() too early in database queries.

 

7. What is the difference between Span<T> and arrays?

Explanation:

An array owns its storage, while Span<T> provides a lightweight view over contiguous memory without copying the underlying data.

Example:

int[] numbers = { 10, 20, 30, 40, 50 };

Span<int> span = numbers.AsSpan(1, 3);

span[0] = 99;

Console.WriteLine(numbers[1]); // 99

Explanation:

Span<int> references a portion of the original array. Modifying the span also modifies the array.

Key Points:

  • Arrays store elements.

  • Span<T> provides a view of existing memory.

  • Creating a span over an array does not copy its elements.

  • Span<T> is a ref struct with lifetime restrictions.

  • Useful for high-performance parsing and buffer processing.

 

8. When should you use ValueTask instead of Task?

Explanation:

Task is the standard return type for asynchronous operations.

ValueTask can reduce allocations when an asynchronous method frequently completes synchronously.

Example:

public ValueTask<int> GetCountAsync(bool cached)
{
    if (cached)
    {
        return ValueTask.FromResult(100);
    }

    return new ValueTask<int>(LoadCountAsync());
}

private async Task<int> LoadCountAsync()
{
    await Task.Delay(100);
    return 100;
}

Explanation:

When the result is cached, ValueTask can return it without creating a new Task<int> object.

Key Points:

  • Prefer Task by default.

  • Consider ValueTask for frequently synchronous completions in performance-critical code.

  • ValueTask has more complex consumption rules.

  • Avoid awaiting the same ValueTask multiple times unless its backing operation explicitly supports it.

  • Use benchmarking before replacing Task.

 

9. What are allocation-free programming techniques?

Explanation:

Allocation-free programming aims to minimize or eliminate heap allocations in frequently executed code.

Common techniques:

  • Use Span<T> for memory slicing.

  • Use stackalloc for small temporary buffers.

  • Use ArrayPool<T> for reusable arrays.

  • Avoid unnecessary boxing.

  • Avoid unnecessary temporary lists and strings.

Example:

// Allocates an array
int[] numbers = new int[5];

// Stack-based buffer
Span<int> values = stackalloc int[5];

values[0] = 100;

Console.WriteLine(values[0]);

Explanation:

stackalloc allocates the buffer on the stack rather than the managed heap, avoiding GC overhead for that buffer.

Key Points:

  • Particularly useful in high-throughput applications.

  • Span<T> enables allocation-free slicing.

  • ArrayPool<T> reduces repeated array allocations.

  • Use stackalloc only for appropriately small buffers.

  • Allocation-free code is not automatically faster; measure the results.

 

10. How do you investigate high CPU and memory usage in a .NET application?

Explanation:

Use profiling and diagnostic tools to determine which methods consume CPU, where memory is allocated, and whether objects are being retained unnecessarily.

Common tools:

Tool

Purpose

Visual Studio Performance Profiler

CPU and memory analysis

dotnet-counters

Runtime counters and GC monitoring

dotnet-trace

Collect execution traces

dotnet-dump

Analyze memory dumps

Application Insights

Application performance monitoring

Example:

# List running .NET processes
dotnet-counters ps

# Monitor a specific process
dotnet-counters monitor -p 1234

Explanation:

Here, 1234 is the process ID.

The tool can display runtime metrics such as CPU usage, GC activity, and memory usage.

Investigation flow:

High CPU / Memory Usage
          |
          v
Check Runtime Metrics
          |
          v
Capture CPU Profile / Memory Dump
          |
          v
Identify Expensive Methods / Objects
          |
          v
Optimize Code
          |
          v
Measure Again

Key Points:

  • High CPU: investigate expensive loops, algorithms, serialization, and GC activity.

  • High memory: investigate allocations, object retention, and memory leaks.

  • High GC frequency: investigate allocation rates.

  • Use production-like workloads when profiling.

  • Compare performance metrics before and after optimization.

 

Quick Interview Revision

Topic

Important takeaway

Performance optimization

Measure, identify bottlenecks, optimize

StringBuilder

Reduces allocations during repeated concatenation

Boxing/Unboxing

Avoid unnecessary boxing using generics

LINQ

Avoid unnecessary materialization and repeated queries

Garbage Collection

Reduce excessive heap allocations

IEnumerable vs IQueryable

In-memory enumeration vs provider-translated queries

Span

Access contiguous memory without copying

ValueTask

Useful when operations frequently complete synchronously

Allocation-free techniques

Span, stackalloc, ArrayPool

CPU/Memory investigation

Use profilers, counters, traces, and dumps

Interview tip: For a Senior .NET Developer or Tech Lead interview, focus especially on LINQ optimization, EF Core query performance, GC allocations, asynchronous programming, and performance profiling. These are practical topics that commonly arise when troubleshooting production applications.

← Back to Articles