Allocating an object on the managed heap is one of the cheapest operations in .NET — but "cheap" is not the same as "free."
You now know the GC reclaims garbage generationally, cheaply, in Gen0 most of the time. But there's a question sitting right behind that: how does new Customer() actually get its memory in the first place? In a language without a GC, allocation itself usually means asking a general-purpose allocator to search for a suitable free block — a real, sometimes-expensive operation. Does .NET do the same thing?
It doesn't — and the reason it doesn't is directly connected to everything you just learned about generations. Because Gen0 is compacted after every collection, allocating into it can be reduced to one of the simplest operations a computer can perform.
In this lesson, you'll see exactly how a Gen0 allocation happens, why it's so fast, and why "fast to allocate" still doesn't mean allocation has zero cost.
Allocating a new object on Gen0 of the managed heap is, in the common case, just moving a pointer forward. The runtime keeps a marker — think of it as "the next free address" — and handing out memory for a new object is as simple as: check there's enough room, hand back the current marker's address, then advance the marker past the space just used. No searching for a free block, no bookkeeping about fragmented gaps — because after every Gen0 collection, the survivors are compacted (moved together), guaranteeing the free space is always one single contiguous region.
This technique is called bump-pointer (or bump allocation). It's only possible because the region being allocated into is guaranteed to have a single contiguous block of free space at its end — a guarantee that generational GC actively maintains through compaction, rather than something that happens to be true by luck. Contrast this with a general-purpose "free list" allocator (closer to what malloc typically does), which has to search among many scattered free blocks of varying sizes to find one that fits — real, variable-cost work that a bump allocator sidesteps entirely.
A typical C# program, especially a web API or any application doing heavy LINQ or string work, allocates constantly — DTOs, intermediate LINQ collections, string interpolation results, closures. If each of those allocations required searching through a scattered free list for a suitably-sized gap, allocation itself would become a real, visible cost sprinkled throughout ordinary code — the opposite of what you'd want for a language that's meant to let you write natural, expressive code without obsessing over every allocation.
Generational GC's compaction (from the previous lesson) isn't just about keeping collections cheap — it has a second, equally important payoff: it keeps Gen0's free space contiguous, which is exactly the precondition bump-pointer allocation needs. This is a deliberate, connected design: the GC does a bit of extra work during collection (moving survivors together) specifically so that allocation, which happens vastly more often than collection, can be as close to free as physically possible. It's a trade: pay a small, predictable cost during (infrequent) collection to make the (extremely frequent) allocation path nearly instantaneous.
var order = new Order(); // triggers a heap allocation
The runtime checks whether the current Gen0 allocation pointer, plus the size of an Order object, still fits within Gen0's budget. In the common case, it does.
The memory address currently held by the allocation pointer becomes order's address. The runtime then advances the pointer forward by exactly the size of an Order object (including its object header — see below), so the very next allocation starts right after it. Conceptually, this is close to a single addition and a comparison — genuinely one of the cheapest operations in the entire runtime.
Every object gets a small fixed-size header (typically holding a pointer to its type's method table and some flags used by the runtime — for locking via lock, for hash code caching, and other bookkeeping). The runtime zeroes out the newly-claimed memory and then your constructor runs, setting the object's actual field values. The zeroing and construction cost is separate from — and typically larger than — the pointer-bump itself, which is worth keeping in mind: "allocation is a pointer bump" describes claiming the space, not the full cost of bringing a new object into existence.
Eventually the allocation pointer reaches the edge of Gen0's budget. A Gen0 collection runs (the previous lesson's topic), reclaiming garbage and compacting survivors into a tight, contiguous block. The allocation pointer is reset to sit right after that block — and the whole cycle of cheap bump allocations begins again against a freshly restored, fully contiguous free region.
public static List<string> FormatNames(List<Customer> customers)
{
var results = new List<string>(); // 1 allocation (the List's internal array too)
foreach (var c in customers)
{
// Each iteration: a new interpolated string is allocated,
// then added — the List itself may also reallocate its
// backing array as it grows past its current capacity.
results.Add($"{c.FirstName} {c.LastName}");
}
return results;
}
What's happening allocation-wise:
List<string> itself and its backing array) — every single one is a cheap Gen0 bump allocation.A high-throughput JSON API deserializing thousands of requests per second is a good example of both sides of this lesson at once. Each request typically allocates: a DTO graph from deserialization, some LINQ intermediate sequences while validating or transforming the data, and a response object. Every one of those allocations is individually near-free thanks to bump allocation — no single line of that code is "slow" in isolation.
But multiply that by tens of thousands of requests per second, and the aggregate allocation rate becomes the actual variable that matters: it directly determines how often Gen0 fills up and needs collecting. A service allocating twice as much garbage per request will trigger roughly twice as many Gen0 collections under the same load — and while each Gen0 collection is individually cheap, at a high enough frequency, that adds up to real, measurable CPU time and can start contributing to tail latency. This is precisely why performance-sensitive .NET code (hot loops in a high-throughput service, not everyday business logic) sometimes reaches for allocation-reducing techniques like object pooling or Span<T> — tools for a later Advanced lesson — specifically to reduce collection frequency, not because any single allocation was expensive on its own.
Bump-pointer allocation is like handing out tickets from a fresh roll — the next ticket is always right there, torn off in one motion, no searching required. That's only possible because the roll is guaranteed unbroken and freshly wound (Gen0's compaction guarantee).
A free-list allocator is more like handing out tickets from a big box where some have already been used and returned, out of order, mixed in with unused ones — every time someone asks for a ticket, you have to dig through the box looking for one that's actually free. That digging is real, variable-cost work the ticket-roll approach never has to do.
Where the analogy needs care: "tearing off a ticket" (the pointer bump) is genuinely near-instant — but writing the recipient's name on it, stamping it, and filing a copy (initializing the object's header and running its constructor) still takes real time. The cheap part is claiming the space, not everything that happens once you have it.
lock and object hash codes).int in a class: the header can be a meaningful fraction of the object's total footprint).This overstates the case. Any single allocation is extremely cheap compared to a general-purpose unmanaged allocator's free-list search — that part is genuinely true and well worth internalizing. But allocation still has real costs: object header overhead, constructor execution time, and — most importantly at scale — contribution to how often Gen0 (and, if survivors keep piling up, older generations) needs collecting. "Cheap per-allocation" and "free in aggregate at any volume" are different claims; only the first one is accurate.
This is true specifically for Gen0, precisely because compaction guarantees a single contiguous free region there. It is not true for the Large Object Heap, which — as you learned in the previous lesson — isn't compacted by default, and does track and reuse freed gaps rather than relying purely on a simple bump pointer. The "it's just a pointer bump" story is a Gen0-specific story, not a universal one across the entire managed heap.
Not necessarily, and not automatically worth chasing everywhere. Contorting readable, idiomatic code to avoid ordinary short-lived allocations, without measurement, frequently isn't worth the loss in clarity — the whole point of generational GC's design is to make this pattern cheap so you don't have to avoid it reflexively. Allocation reduction is a targeted optimization for measured hot paths, not a default coding style to apply everywhere.
Rewriting a perfectly readable LINQ pipeline into a manual, allocation-avoiding loop for code that runs a handful of times per request, on the theory that "fewer allocations is always better."
Reserve allocation-conscious rewrites for code proven, through profiling, to be a genuine hot path. Elsewhere, prioritize readability — the GC is specifically engineered to make this the right default trade-off.
Ignoring a high allocation rate in a genuinely hot, high-throughput path because "each allocation only takes a few nanoseconds."
Recognize that the aggregate allocation rate is what drives collection frequency — a high enough rate of cheap individual allocations still adds up to a real, measurable amount of total GC activity, which is exactly why it's worth profiling actual allocation volume, not just per-allocation cost, in hot paths.
Calculating expected memory usage for a large collection of small wrapper objects purely by summing field sizes, and being surprised when actual usage is meaningfully higher.
Account for the fixed per-object header overhead, especially for collections of many small objects — this is one concrete reason a struct (no separate header, when not boxed) can be meaningfully more memory-efficient than an equivalent class for very small, high-volume data, a distinction the stack-vs-heap and boxing lessons build on directly.
You don't control the allocation mechanism directly — but understanding it correctly calibrates when allocation is genuinely worth optimizing:
You've learned how Gen0 allocation actually works, and why "cheap" isn't the same as "free." Let's check your understanding.
1. What makes bump-pointer allocation possible for Gen0, specifically?
Correct: B
Why B is correct: Compaction is what guarantees Gen0's free space is a single contiguous block after every collection — that guarantee is the entire precondition that makes a simple pointer-bump sufficient, with no search required.
Why A is incorrect: Objects vary widely in size; bump allocation handles variable sizes fine — it just advances the pointer by however many bytes the specific object needs.
Why C is incorrect: There's no dedicated core reserved specifically for allocation — allocation happens on whichever thread is running the allocating code (potentially using thread-local buffers, covered under the hood).
Why D is incorrect: Allocation and collection are related but distinct events — collection isn't "disabled" during allocation; rather, allocation simply consumes the free space that the most recent collection made contiguous.
Reinforcement: Compaction and fast allocation are two sides of the same design decision — the GC does extra work during (infrequent) collection specifically to make (extremely frequent) allocation nearly free.
2. Which statement most accurately describes the relationship between "allocation is cheap" and application performance?
Correct: B
Why B is correct: This is the precise, non-overstated claim the lesson makes: individual allocations are cheap, but volume still matters because it drives collection frequency — and collection frequency is a real, aggregate cost, even when each individual collection is itself cheap.
Why A is incorrect: This is the overstated version of the claim the lesson explicitly warns against — volume absolutely does matter in aggregate, even though each allocation is cheap in isolation.
Why C is incorrect: The CLR does not pre-allocate all needed memory upfront — memory is allocated on demand as objects are created, and generations grow their budgets over time as needed.
Why D is incorrect: This option conflates two unrelated ideas from different lessons — the LOH's non-compaction is a separate topic from whether general allocation cost scales with volume.
Reinforcement: The nuanced, accurate position — "cheap per-op, but volume still matters in aggregate" — is exactly what separates a well-calibrated understanding of GC performance from either extreme (fearing all allocation, or ignoring allocation entirely).
3. Why might a collection of one million tiny wrapper objects (each holding just a single int field) use noticeably more total memory than "one million times the size of an int" would suggest?
Correct: B
Why B is correct: Every heap object carries fixed header overhead beyond its field data. For a tiny object like a single-int wrapper, that per-object overhead can be a large proportion of the object's total footprint — exactly the scenario described under the hood as a reason to consider a struct instead, for very small, high-volume data.
Why A is incorrect: There's no arbitrary "safety margin" doubling applied to object sizes — the actual overhead comes specifically from the object header, not a padding policy like this.
Why C is incorrect: An int field genuinely stays 32 bits inside an object — it isn't silently widened to 64 bits; the extra memory comes from the object header, not from resizing the field itself.
Why D is incorrect: These tiny wrapper objects, at that size, would be ordinary Gen0 objects, not LOH objects (which require ≥ 85,000 bytes) — LOH padding behavior isn't relevant here.
Reinforcement: Object header overhead is a concrete, measurable reason that "many tiny reference-type objects" can be a real memory cost worth being aware of — one of the practical payoffs of understanding allocation mechanics.
4. A developer rewrites a clear, idiomatic LINQ pipeline into a manually optimized allocation-avoiding loop, inside a method that runs only a few times per user request in an otherwise typical web application. Based on this lesson, how should this decision be evaluated?
Correct: B
Why B is correct: This is precisely the "Common Mistake" called out in the lesson — premature, unmeasured allocation elimination in ordinary, low-volume code, trading real readability for a performance gain that generational GC's design was specifically built to make unnecessary to chase in this scenario.
Why A is incorrect: The lesson explicitly rejects "fewer allocations always better" as an oversimplification — the right call depends on whether the code path is actually a measured hot path.
Why C is incorrect: LINQ does allocate (intermediate sequences, iterator state, etc.) — that's a real, well-understood behavior, not a bug; the rewrite claim doesn't follow from anything in this lesson.
Why D is incorrect: Code structure genuinely does affect allocation patterns and performance — the lesson's point isn't that it never matters, but that it matters far less in low-volume paths than intuition might suggest.
Reinforcement: The calibrated response to "should I reduce allocations here?" is always "is this a measured hot path?" — not a blanket yes or no.
5. Does the "bump-pointer, no searching needed" description of allocation apply equally to every part of the managed heap?
Correct: B
Why B is correct: The bump-pointer story depends entirely on the contiguous-free-space guarantee that compaction provides — a guarantee the LOH doesn't share, since it isn't compacted by default. The LOH instead has to track and reuse freed gaps, closer to a free-list style approach, exactly as covered in the previous lesson on generational GC.
Why A is incorrect: This overgeneralizes a Gen0-specific mechanism to the entire heap, explicitly contradicted by the LOH's different, non-compacting behavior.
Why C is incorrect: Bump-pointer allocation applies to reference-type objects landing on the managed heap — it isn't about value types at all, which (when they avoid the heap) don't need heap allocation in the first place, the topic of the next lesson.
Why D is incorrect: Both Server and Workstation GC modes rely on the same underlying generational, compaction-based design for Gen0/Gen1 — the difference between the modes is about heap/thread count and parallelism, not the fundamental allocation mechanism.
Reinforcement: This connects directly back to the previous lesson — the LOH's default non-compaction (there, framed as a collection-cost trade-off) has a direct, matching consequence here on the allocation side too.
You now understand exactly how cheap — and how not-quite-free — heap allocation really is in .NET. Next: when a value doesn't need the heap at all.
dotnetmadeeasy.com — Learn C# and .NET, the right way.