C# Static Lambda Expressions for High Performance
Static lambda expressions, introduced in C# 9.0, provide a way to
ensure that anonymous functions do not capture local variables or
enclosing instance state. This article explains how static lambdas work,
how regular lambdas cause hidden heap allocations through closures, and
why applying the static modifier to your lambdas is an
effective strategy to eliminate memory overhead, reduce garbage
collection pressure, and guarantee high-performance execution paths.
What Are Static Lambda Expressions?
In C#, a standard lambda expression can access variables outside its
immediate scope. When a lambda references a local variable, parameter,
or this instance field from its enclosing context, the C#
compiler creates a closure. A closure generates a hidden heap-allocated
class (often called a display class) to hold both the captured state and
the method.
A static lambda expression explicitly prevents this behavior. By
prefixing a lambda declaration with the static keyword, you
instruct the compiler to disallow the capture of any enclosing
state:
// Standard lambda (can capture outer variables)
Func<int, int> square = x => x * x;
// Static lambda (guaranteed not to capture outer variables)
Func<int, int> staticSquare = static x => x * x;If you attempt to reference a local variable or instance member inside a static lambda, the compiler raises an error (CS8828: "A static local function or lambda cannot contain a reference to '<variable>'").
Why You Should Use Static Lambdas for Performance
1. Elimination of Unintended Heap Allocations
The primary performance hazard of standard lambdas is unintentional memory allocation. Capturing an outer variable allocates a new display class object on the managed heap every time the enclosing method runs:
public void ProcessData(List<int> numbers)
{
int factor = 2;
// Allocates a display class on every method invocation
var results = numbers.Select(n => n * factor);
}By switching to a static lambda, the compiler stops you from
capturing factor. This forces you to pass the required
state explicitly through method parameters or design patterns that do
not allocate memory:
public void ProcessData(List<int> numbers)
{
const int factor = 2; // Constants can be used because they don't require capture
var results = numbers.Select(static n => n * factor);
}2. Automatic Delegate Caching
When a lambda captures no state, the C# compiler optimizes execution by caching the delegate instance in a private static field. The delegate is instantiated only once for the lifetime of the application domain, reducing allocation overhead to zero for subsequent executions.
Using the static modifier guarantees that the lambda
qualifies for this optimization. It ensures that future code
modifications do not accidentally introduce a capture that disables
delegate caching.
3. Compile-Time Safeguards in Hot Paths
In high-throughput scenarios—such as game loops, web request pipelines, and message processing systems—garbage collection pauses can degrade performance. Using static lambdas serves as a compile-time assertion. It protects performance-critical code from regressions, ensuring that other developers cannot inadvertently introduce closures during refactoring.
When to Use Static Lambdas
Static lambdas are best suited for:
- LINQ queries in performance-sensitive areas:
Ensuring transformations (
Select,Where) reuse delegates without hidden costs. - APIs accepting state parameters: Methods like
MemoryCache.GetOrCreateorThreadPool.QueueUserWorkItemthat accept a state object along with a delegate. Passing state explicitly via arguments combined with a static lambda yields zero allocations. - Logging and metrics frameworks: Evaluating deferred values without holding onto scope references.
Adopting static lambdas by default when state capture is not required keeps allocations predictable, lightens the load on the garbage collector, and makes the performance characteristics of your C# code explicit.