C# LINQ, Collections, and Allocation Costs
For LINQ to Objects, distinguish constructing a query from enumerating it, and distinguish deferred execution from streaming behavior. Repeating enumeration can repeat source work. Materializing with an array or list can avoid later source traversal while adding storage and changing when values are observed.
Start by asking how often the result is consumed and whether the source can change. Then inspect the actual operators and measure the relevant cost. Counting selector visits can explain repeated work, but it does not measure allocated bytes, collection pressure or elapsed time.
TL;DR
- Constructing a LINQ query is not running it: most operators use deferred execution.
- Deferred isn't the same as streaming; operators such as
OrderByandGroupBybuffer their input. - Enumerating a query twice repeats the source work;
ToList()orToArray()materializes once but adds storage and fixes values at that moment. - Decide how often a result is consumed, and whether the source can change, before materializing.
- Measure allocations and time with a profiler or benchmark; counting visits explains repeated work only.
Quick Example
This C# program targets .NET 10. It uses LINQ to Objects over a List of integers and checks source visits and materialized values. It accesses no database or external service.
The two Sum calls visit the original four inputs twice. Materializing visits them once more, after which consuming the array does not repeat the selector. Adding a fifth source item changes the next enumeration while the existing integer array retains three elements. The final observed visit count was seventeen. No allocation or timing benchmark was performed.
Core Concepts
Deferred does not mean already computed
Microsoft describes deferred execution as postponing evaluation until the value is needed. The query can represent work to perform later rather than a stored result. Its behavior therefore depends on when and how the consumer enumerates it. Deferred execution
In the example, creating query leaves visits at zero. Each Sum call then asks for the sequence and runs the selector for the current source. An IEnumerable interface alone does not promise that results are cached, repeatable, cheap, or independent of source state.
Deferred and streaming describe different boundaries
An operator can defer work until enumeration while still needing to consume substantial input before producing its first result. Microsoft's materialization discussion uses OrderBy as an example that processes and sorts its source before yielding the first item. Intermediate materialization
Review the complete pipeline when first-result latency or memory matters. A chain that begins with a streaming filter can later reach a buffering sort. A final Take does not automatically remove work that an earlier operator must perform. Verify the actual operator behavior and runtime rather than inferring it from fluent syntax.
The table is an editorial decision map. It does not provide measured allocation or performance comparisons.
Repeated enumeration can affect correctness
Microsoft's CA1851 guidance notes both performance costs and possible bugs when enumeration is expensive or has side effects. It discusses materialization as one possible response, with its own time and storage cost. CA1851 guidance
The example uses a side-effect counter to make evaluation visible. In application code, a source may read changing data or perform another observable operation. Determine whether consuming it twice is semantically intended before treating the issue as a micro-optimization.
Materialization is not a universal deep snapshot. This integer array preserves the values it captured, but materializing references does not automatically clone the objects they reference. Specify the required stability and ownership rather than assuming ToArray freezes every reachable object.
Allocation claims require measurement
Identify where collections, buffers, closures, boxing or other representations may be created in the actual code path. Treat those as hypotheses to inspect, not a fixed allocation count inferred from the number of LINQ calls. Implementation details and optimizations can vary across types and runtime versions.
Use an appropriate benchmark or profiler for the actual question. Control inputs, correctness, warmup, runtime and measurement conditions. A single elapsed-time sample or a selector counter cannot establish a reliable bytes-per-operation result. The article deliberately makes no such numerical claim.
Best Practices
Make consumption frequency visible in review. If a method counts, logs and processes one deferred sequence separately, inspect whether that means several enumerations. Combine work or materialize when the semantics and workload justify it. Do not add a list merely because an analyzer highlighted a possible path.
Use collection capabilities intentionally. If the caller already has a suitable array or collection, its available operations may differ from an arbitrary generator. Preserve API contracts and avoid casts that assume a source type not guaranteed by the method signature.
Separate LINQ to Objects from provider-backed execution. A database query can translate operations, execute remotely and have its own tracking and materialization rules. The local List example provides no query-plan, round-trip or ORM allocation evidence.
Test source mutation and repeated consumption when they matter. Include empty results, one element, multiple consumers and exceptions during enumeration. Keep any side-effecting test instrumentation confined to the fixture so it does not become accidental production logic.
Reverse an eager-materialization choice when the result is large, consumed only once, or must reflect later source state. Reverse a deferred-reuse choice when the operation should observe one stable result or would otherwise repeat expensive work. The decision follows the consumption contract and measured consequences.
Common Mistakes
Treating a query variable as cached data
Calling every deferred operator streaming
Reporting source visits as allocations
Generalizing from LINQ to Objects
FAQ
How do I measure LINQ allocation costs?
Use a benchmarking tool with a memory diagnoser, such as BenchmarkDotNet's [MemoryDiagnoser], on representative data and the runtime you deploy. Counting selector visits, as the example does, explains repeated work but doesn't measure bytes allocated or time.
Should I always call ToArray to avoid repeated work?
No. Materialization adds storage and changes observation timing. Use it when the actual reuse and stability requirements justify the tradeoff.
Were allocation costs measured?
No. The program checks evaluation counts and values only. Actual byte allocation, GC behavior and timing require separate representative measurement.