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/awaitfor 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:
-
stringis immutable. -
StringBuilderis mutable. -
Use
StringBuilderfor 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 ofCount() > 0when 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:
|
|
|
|---|---|
|
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:
-
IQueryablesupports database-side filtering with EF Core. -
IEnumerableis 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 aref structwith 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
Taskby default. -
Consider
ValueTaskfor frequently synchronous completions in performance-critical code. -
ValueTaskhas more complex consumption rules. -
Avoid awaiting the same
ValueTaskmultiple 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
stackallocfor 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
stackalloconly 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 |
|
|
Runtime counters and GC monitoring |
|
|
Collect execution traces |
|
|
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.