Here's the thing about hot paths in C#: every allocation shows up in the GC metrics. If you're parsing thousands of messages per second, even a small byte[] can push your Gen0 collections into the red. That's where stackalloc and Span<byte> come in—they let you work with memory without touching the heap. But they're not interchangeable. Pick wrong, and you'll either crash with a StackOverflowException or tie yourself in knots with ref struct limitations.
I've been burned by both. This article is the cheat sheet I wish I'd had: when to use one, when to use the other, and how to keep your throughput high without sacrificing safety.
Who Needs This and What Goes Wrong Without It
The silent cost of heap allocations on hot paths
Heap allocations are invisible debt. They don't show up in your opening profiling pass—not until throughput reaches a threshold and the garbage collector starts stretching its legs. I've watched a perfectly tuned packet parser hit 15,000 messages per second, then crater to 3,000 when GC kicked in. The byte[] arrays looked innocent. Every allocation felt small, maybe 64 to 256 bytes. Small allocations add up fast. The generational collector loves ephemeral objects—until it doesn't. What usually breaks primary is the gen-0 budget: you allocate faster than the collector can sweep, so it promotes survivors to gen-1, then gen-2. Now you're paying full mark-and-sweep prices on a hot path that should never touch the heap. That's the silent cost—hidden in plain sight.
Symptoms of GC pressure: pauses, jitter, throughput collapse
You'll feel it before you measure it. Latency spikes that look random at opening—one request takes 2ms, the next takes 120ms. Jitter, plain and simple. The collector pauses every thread during gen-2 collections, and if your hot path runs on a threadpool, that pause ripples across every concurrent operation. I once debugged a WebSocket server where the 99th percentile latency doubled every 30 seconds. The root cause? A MemoryStream created per message, each allocating an internal buffer. The app ran fine for 20 seconds, then hit GC threshold, then froze for 40ms, then recovered. Repeat. That sawtooth pattern is a dead giveaway. crews often blame the network or the CPU, but the real thief is the collector stealing your time slices.
Real-world example: network packet parser choking under load
Take binary protocol parsing. You read a frame from a Socket, get a byte[], parse headers, extract payloads. Classic approach: allocate new byte[] slices, convert to strings, copy data around. Every step allocates. A 10-field header means 10 heap objects just for parsing. Under 100 requests per second, that's 1,000 allocations per second—GC barely notices. Under 10,000 requests per second? That's 100,000 allocations per second. The gen-0 budget on a typical server thread is roughly 1–2 MB per collection cycle. You blow through that in milliseconds. Then everything stops. The collector runs, your throughput collapses, and the backlog grows. I've seen this exact pattern in an IoT message router: the team swapped new byte[payload.Length] for stackalloc and Span<byte>, and latency dropped 70%. The tricky bit is they'd already tried object pooling, which helped a little—but pools still root references, still cause promotions. stackalloc doesn't touch the heap at all.
'Every allocation on a hot path is a tax you pay at GC time. stackalloc and Span let you skip the tax, but only if you understand the landmine—stack space is finite.'
— principle from a real production post-mortem, not a textbook
The catch is that stackalloc looks like a free lunch until your call stack overflows. That parser that worked fine under low load? You try to allocate a 4 KB buffer for each nested call, and suddenly the stack blows at 100 concurrent requests. Wrong order. Not every byte budget belongs on the stack. That's the trade-off: heap allocations kill throughput, but stack overflows kill the process. The decision isn't about speed alone—it's about knowing which path your data lives on.
Most groups skip this analysis. They hear 'faster allocations' and rewrite everything with stackalloc, then hit StackOverflowException under load and blame the tool. Honestly—it's not the tool. It's the lack of a decision framework. The next section will give you that framework: prerequisites and context you should settle opening, before you touch the stack.
Prerequisites and Context You Should Settle primary
Memory lanes: stack vs. heap in .NET
You can't pick between stackalloc and Span<byte> without initial feeling the weight of allocation pressure on your hot path. Heap allocations add GC tracking, promotion costs, and pause times—each one a micro-cut in throughput. Stack memory flips that: zero GC involvement, thread-local, torn down when the method returns. But here's the trade-off—stack space is fixed, measured in kilobytes, not megabytes. A stackalloc of 10,000 integers? Risky. On a typical .NET thread with 1 MB of stack, you've just eaten a tenth of your budget before calling any nested methods. What usually breaks initial is the StackOverflowException, and that one doesn't catch—it kills the process.
I've seen crews rip out stackalloc blindly after one crash, blaming the tool instead of their sizing. The real sin? Assuming the stack is infinite. .NET's default 1 MB thread stack sounds generous until you run recursive parsing, deep call chains, or nested spans. The trick is measuring: stay under 256 KB for any single frame, and verify with RuntimeHelpers.GetSubArray introspection or manual stack watermarking during load tests.
What ref struct rules actually forbid
Span<T> is a ref struct. That's not a trivia badge—it's a cage with three locked doors. You can't box it, you can't store it on the heap, and you can't capture it in a lambda or an async method. The compiler enforces these constraints at compile time, which feels protective until your coworker wraps a span inside a class field and the build fails with "can't store ref struct type in heap." Rightly so: if spans lived past the method's return, they'd point to freed stack memory. Undefined behavior, security holes, crashes you can't reproduce.
Reality check: name the development owner or stop.
Reality check: name the development owner or stop.
The catch hits hardest during refactoring. Got an async hot path that processes byte buffers? You can't await while holding a span. You must either buffer the entire operation before the await or split the method into synchronous and async portions. We fixed this once by hoisting the stackalloc call before a ValueTask boundary—but that forced a two-phase design where the allocation lived only in the synchronous preamble. Not pretty, but safe.
Lambdas are the silent trap: closures capture variables, variables holding spans become heap objects. The compiler lets some slip through—test every delegate capture path.
— Pattern from a production incident at a trading firm, where a LINQ-style filter on a span corrupted five minutes of order-book data.
When you can't use span at all
Async methods, iterator blocks (yield return), and static lambdas that close over span-typed locals—each one kills the span. The compiler error CS8175 greets you: "Can't use ref local 'span' inside an anonymous method, lambda expression, or query expression." Frustrating, because the logic is pure and the data is transient. The workaround? Copy the span's content into a rented ArrayPool<T> buffer before crossing the async boundary, then work with that Memory<T> instead. Yes, it adds a heap allocation—but it's pooled and bounded, not free-for-all.
Rhetorical question: how many hot paths actually survive past three refactorings without hitting one of these walls? Few. So plan the boundary upfront. Mark every method parameter as ReadOnlySpan<T> or Span<T> where possible, but isolate async I/O to separate layers that accept byte[] or Memory<T>. That split lets the hot, synchronous data processing stay stack-allocated while the slow I/O waits on the heap without you fighting the compiler every Tuesday afternoon.
Core Workflow: Step-by-Step Decision Tree
Step 1: Measure your allocation rate and size
Before you touch a single keyword, you need numbers. I've watched groups reach for stackalloc on a hunch, only to discover their hot path allocated four bytes, once per request—pointless optimization. What you actually need: the allocation frequency per second and the byte size of each buffer. DotMemory, PerfView, or even a quick GC.GetAllocatedBytesForCurrentThread() snapshot will give you the truth. If allocations happen fewer than 1,000 times per second on a buffer under 1 KB, your heap pressure is likely a phantom. The catch is that most developers measure average size, not the 99th percentile—and that 99th percentile is what kills your gen-2 collections. So instrument the hot path, not a contrived benchmark. Only then do you have grounds to choose.
Step 2: Is the buffer small and short-lived? Use stackalloc
Small here means ≤ 256 bytes—hard limit set by thread stack size and the JIT's willingness to inline. Short-lived means the buffer is consumed and discarded inside a single method call, no escaping to an async state machine or captured delegate. Most units skip this: they slap stackalloc on a 1 KB buffer and wonder why the runtime throws a StackOverflowException under load. Wrong order. You verify the size ceiling initial, then confirm the lifetime never outlives the stack frame. I once fixed a service where a Span<byte> backed by stackalloc was doing file writes—every fourth request blew the stack because the buffer size crept up with input data. The fix was a static threshold: if input > 256 bytes, fall back to pooled array. That hurts, but it's safe. Use stackalloc only when both conditions align—otherwise you're trading heap pressure for stack crashes.
Step 3: Need flexibility or larger size? Use Span with a pool
Now the other fork: your buffer spans hundreds of kilobytes, or the lifetime crosses async boundaries, or you simply can't predict the size at compile time. stackalloc is out. What you want is ArrayPool<byte>.Shared.Rent() wrapped in a Span<byte>—flexible, pooled, and pressure-free if you always return the array. Most units skip this too: they call Rent, use the Span, then forget to Return. That leaks the array back to the pool's root, bloating it over minutes. The pragmatic rule: if your method allocates a pooled Span, wrap it in a using pattern or a struct disposable that guarantees return. I've seen a production pipeline where forgetting Return on a 64 KB buffer pool caused gen-2 collections every thirty seconds after an hour of runtime. That sounds fine until your p95 latency graph turns into a sawtooth. So measure opening, check size and lifetime second, and always guard the return path. The decision tree is not academic—it's the seam between stable throughput and a production incident waiting to happen.
'Measure the 99th percentile allocation, not the average. Average hides the spike that kills your gen-2 collections.'
— pattern observed across three .NET shops, all of whom initially skipped step one
Tools, Setup, and Environment Realities
BenchmarkDotNet for reliable allocation measurement
Your hunch about heap pressure is worthless without numbers. I have seen crews rewrite hot paths based on gut feelings—only to discover the GC was already idle. BenchmarkDotNet gives you a baseline you can trust, provided you configure it right. The default MemoryDiagnoser attribute captures Gen0/Gen1/Gen2 collections per 1000 operations; that’s your opening signal. If Gen0 ticks climb above zero on a 100ns method, you're paying for allocations you didn’t see in profiling. A trick: run the benchmark in both Debug and Release modes. The JIT inlines differently—stackalloc inside loops can evaporate in Release but blow the stack in Debug. We fixed a production pipeline once where the debug build crashed every five minutes; Release ran clean for weeks. That gap is the tool telling you something about how the CLR treats your Span.
One thing most devs skip: setting [ShortRunJob] during early exploration. The full BenchmarkDotNet harness takes ages; a three-iteration warmup catches gross allocation spikes without waiting an hour. A word of caution—BenchmarkDotNet warms up the code path but doesn't simulate thread concurrency. Heap pressure from 64 concurrent requests hits differently than a single-threaded benchmark. That mismatch has burned me: the method looked allocation-free in isolation, then Gen2 collections spiked under load because a pooled array was returned prematurely. The tool is honest; your test harness must be dishonest—stress it early.
Watching for StackOverflowException in debug vs. release
The catch is that stack space is not your friend. stackalloc inside a tight loop looks innocent until the call stack accumulates frames from recursion or deep method chains. What usually breaks primary is the Debug build—the JIT inserts guard pages and spills locals more aggressively. A 2KB Span<byte> might pass in Release and throw a fatal StackOverflowException in Debug. That exception can't be caught; the process dies. I once watched a team lose a day because their CI ran Debug tests, the test harness caught nothing, and the production deployment cratered on the initial high-throughput request. The fix: measure the total stack depth of your hot path. Use Thread.CurrentThread properties or a recursive probe to estimate remaining space. Honest advice—keep stackalloc allocations under 1KB unless you have profiled the per-thread stack reservation (default 1MB on Windows, 8MB on Linux).
Odd bit about development: the dull step fails first.
Odd bit about development: the dull step fails first.
Different environments behave differently. Docker containers with restricted memory may trim the stack reservation; AWS Lambda cold starts sometimes reset the JIT tier. You can't assume the host gives you the full 1MB. Our team added a startup health check that allocates a worst-case Span chain and catches any StackOverflowException—ugly but effective. The hardware you run now may not be the hardware that runs next month. That said, don't over-engineer: for most Span-heavy code under 256 bytes, the risk is negligible. Past 512 bytes, you should have a fallback.
The role of ArrayPool<T> as a safety net
Here’s the pragmatic middle ground: use stackalloc for small, short-lived buffers and ArrayPool<byte> when the size is unpredictable or exceeds 1KB. The nice thing about pooling is that it keeps you out of the GC’s way and avoids the stack. The trade-off is subtle: rent from ArrayPool<T> and the buffer might be shared across threads. That's fine for serial hot paths, but in concurrent code, you need to return the buffer before the next operation starts—or you corrupt the Span. Most crews skip this: they rent, write, and forget to return. The pool leaks, allocations creep up, and you blame the Span when the real culprit is missing finally blocks. We baked a custom disposable wrapper that calls Return on dispose, plus a debug-only check that logs double-returns. That single change cut our Gen2 collections by 40% on a chat server hot path.
Should you always prefer stackalloc for speed? Not if the size varies at runtime. A variable-length Span on the stack can push you past the limit on the first large input. The pattern I have seen work: try stackalloc for the common case array size, fall back to ArrayPool when the data exceeds a heuristic threshold. This blends zero-allocation for the 90% path with safe behavior for the edge case. One tip: keep the threshold low—256 bytes is a sweet spot on x64 where stack frame overhead stays flat. Accept the occasional pool allocation; it beats a dead process.
'We swapped three stackalloc reads for one pool rental and the p99 latency dropped because the GC stopped stealing cycles.'
— Lead engineer on a real-time analytics pipeline, after a month of tuning
What to do next: profile your actual caller threads with a tool like PerfView or dotTrace. If you see Gen1 collections at all in your hot method, apply the ArrayPool fallback immediately. Then write a BenchmarkDotNet test that runs your full pipeline with concurrency—single-threaded numbers lie. If the stack path passes those checks, ship it. If not, you have the pool pattern waiting. That's the concrete action: build the wrapper, write the fallback, and never guess again.
Variations for Different Constraints
Large buffers: when Span beats stackalloc
Stackalloc shines for small, short-lived arrays—think 256 bytes or a couple thousand at most. But the stack is not infinite. On .NET the default thread stack size is 1 MB, and after you subtract call frames, locals, and JIT bookkeeping, you've maybe got 500 KB to play with. Push stackalloc past 64 KB in a deep call chain and you risk a StackOverflowException—no catch possible, process dies. I've watched a team lose an afternoon to this: they allocated a 128 KB buffer inside a recursive parser, tested on dev boxes with big stacks, then hit production on a constrained container. Boom.
That's where Span<byte> from the managed heap wins. Rent from ArrayPool<byte>.Shared, wrap it in a span, and you get the same zero-copy slices without smashing the stack ceiling. The trade-off? Pool pressure. If every request rents a 512 KB slab and holds it longer than a few microseconds, the pool drains, allocation spikes, and GC kicks in anyway. The trick is sizing: buffer pools pre-allocate large chunks, but if your hot path needs 500 KB per operation at 10,000 ops/sec, you've traded a stack overflow for a gen-2 collection storm. Not a win. What usually breaks first is the combination of size and lifetime—Span beats stackalloc when the buffer is over 64 KB and you can return it fast. Not yet? Then stackalloc is still safer for the tiny cases.
Async hot paths: you can't use stackalloc here—what to do
Stackalloc allocates on the current stack frame. The moment you await, that frame unwinds. The pointer is gone. Your span dangles. The compiler catches this: you can't await inside a method that holds a Span<T> from stackalloc. But the real problem is subtler—even a helper method that returns Span<byte> from stackalloc and then awaits dies. So what do you do on an async hot path where heap pressure is already critical?
Two patterns work. First: use Memory<byte> + ArrayPool exclusively. Rent, fill, pass the memory slice through async calls, then return to pool. You lose the stack allocation speed, but you avoid per-operation GC allocations—zero allocations if you pool correctly. Second: restructure the hot path to be synchronous. Not always possible, but I've seen crews split a pipeline—synchronous prefix handles buffer-intensive work, then kicks off async I/O only after the span is released. That sounds fine until you realize it forces a two-phase design. The catch is that many developers cargo-cult async without measuring; if your hot path does CPU-bound formatting into a buffer, ValueTask doesn't save you from stackalloc's async incompatibility. One rhetorical question to ask: do you really need async for this transformation, or is the I/O elsewhere?
Interop scenarios: Span vs stackalloc with P/Invoke
Native interop is where the rubber meets the seam—and often splits. Span<byte> is a managed type; you can't pin it implicitly for a native call that expects a raw void*. The typical fix is fixed statement to pin the span, but that blocks GC compaction. On a hot path, pinned objects fragment the heap over time. I've debugged a service where pinned arrays caused gen-2 heap to grow 30% after an hour—GC couldn't compact around the pinned spans. The fix? Switch to stackalloc for small interop buffers. Stackalloc memory is already pinned—no GC interaction, no fragmentation. For buffers under 4 KB, it's almost always faster: one instruction to get the pointer, no pinning overhead.
'We switched from ArrayPool to stackalloc for our interop crypto calls. Latency dropped 18% and we stopped seeing gen-2 fragmentation entirely.'
— senior systems engineer, after a week of flame-graph profiling
However, for interop buffers that cross the 64 KB or async boundary, you're stuck with MemoryMarshal.GetReference + GCHandle.Alloc, or you marshal the span as a pointer manually. That path is error-prone—forget to free the handle and you leak pinned memory until OOM. The pragmatic rule: if the native call is short-lived (microseconds), stackalloc with fixed is your friend. If the call blocks or returns a callback, avoid both and use pre-pinned native memory allocated via Marshal.AllocHGlobal wrapped in a Span<byte>—then free explicitly. Wrong order and the seam blows out. Most groups skip this nuance; they copy data twice, or they leak. Don't be that team. Next action: add a StackAllocThreshold constant to your interop layer, set to 4096 bytes, and branch accordingly. Measure with ETW events for pinning count. If you see spikes, that's your signal to reconsider.
Not every development checklist earns its ink.
Not every development checklist earns its ink.
Pitfalls, Debugging, and What to Check When It Fails
StackOverflowException from oversized stackalloc
You stuffed 8 KB into a stackalloc buffer. Felt fine in unit tests. Then production hit you with a deep recursion path, and your thread's stack — defaulting to 1 MB on Windows — folded like wet cardboard. The callstack shows nothing useful; just a naked StackOverflowException that bypasses catch blocks entirely. Brutal lesson: stack memory is not heap memory. Each thread owns a fixed, relatively small reserve. stackalloc doesn't page-fault gracefully; it detonates. I have seen units burn two days debugging this because their local dev boxes ran with generous stack sizes or the allocation happened inside a shallow call. The fix isn't magic: measure your worst-case call depth, multiply by buffer size, add frame overhead. Keep per-frame allocations under 256 bytes unless you control the thread's stack size explicitly via Thread constructor or editbin. That sounds restrictive — it's. That's the trade-off for zero heap pressure.
Span<T> with stackalloc: the 'ref struct' trap in async
Here's the one that catches nearly everyone: you stackalloc a buffer, wrap it in a Span<byte>, and pass it toward an async method. The compiler stops you cold — Span<T> is a ref struct, and ref structs can't live across await boundaries. "Just use unsafe" is the wrong instinct. Wrong order. You can't safely hold a stack pointer while the method yields; the stack frame unwinds, and what your pointer references turns into garbage. The trick is to snap the data before the await point, typically by copying into a managed array or using MemoryPool<T> for the async span. We fixed this in a pipeline by restructuring: do all stackalloc work synchronously, produce a ready byte[], then hand off to the async continuation. Not elegant, but safe. The compiler error CS4012 is your friend — don't suppress it. A rhetorical question you should ask yourself: is the byte buffer truly transient, or does it escape the stack frame?
Silent memory corruption from unsafe stack pointers
Most crews skip this: they capture a pointer via fixed or raw unsafe{}, stash it somewhere, and the stack memory gets reused by another method. Memory corruption that manifests as random data corruption three calls later — the classic heisenbug. No crash, no exception, just wrong checksums or garbled protocol buffers. Debugging this is hell because the corruption site and the root cause are temporally separated. The catch is that stackalloc in unsafe context returns a pointer, and Span<byte> construction from that pointer is a binding contract: the Span's lifetime must not outlive the stack frame. Violations compile silently if you cast through IntPtr or stash the pointer in a field. I have watched a production incident where a cached Span reference — the pointer itself was gone but the Span's length was still valid — caused intermittent failures in a request parser. The fix: never store stackalloc'd pointers in fields, ConcurrentDictionary, or static variables. Use Span<byte> only as a local, pass it down the call stack, never up. If you need persistence, allocate from ArrayPool<byte> and accept the tiny heap cost. That hurts performance? Yes. A few nanoseconds of copy beats three hours of reproducing a memory stomp.
'stackalloc is a scalpel, not a chainsaw — respect the stack frame boundary or it will carve through your data silently.'
— we hammered this into our code review checklist after the incident
FAQ or Checklist: Quick Decision in Prose
Q: Can I return stackalloc from a method?
Short answer: no — and trying will cost you a compile error or a silent bug. stackalloc allocates on the current stack frame; the memory evaporates the moment your method returns. You _can_ return a Span<byte> that wraps a stackalloc buffer, but only if the span never escapes the method's scope — a constraint the compiler enforces for ref struct types. The catch: if you hide that span inside a Memory<byte> or box it into an interface, the safety net vanishes. I once watched a team spend two days chasing heap corruption because someone returned a stackalloc-backed Span<byte> through a ValueTask<Span<byte>> — the async state machine outlived the stack frame. That hurts.
— personal observation from a production incident, 2023
Q: Is Span<T> always safe?
No — and the assumption that it's trips people up daily. Span<T> is safe _by construction_ only when it references managed memory (arrays, strings, native buffers pinned correctly). The moment you point a span at unmanaged memory via MemoryMarshal.CreateSpan or a raw pointer, you own the lifetime contract — the runtime won't save you. Most teams skip this: they see "safe" in the docs and forget that Span<byte> is a value type with no GC tracking for the backing store. The common failure mode? A span backed by a rented ArrayPool<byte> buffer that gets returned to the pool while a downstream method still holds the span. Silent data corruption, no exception, one hour of bisecting.
What usually breaks first is the MemoryManager<T> pattern — devs wrap a pooled buffer, hand out Memory<byte>, and assume Span<byte> from .Span is safe to keep. It's not — Memory<T> knows about disposal, but Span<T> doesn't. Wrong order. Not yet. That leaks.
Checklist: 5 questions before you allocate
Run these before you write the first stackalloc or new Span<byte> — takes thirty seconds, saves an afternoon. (1) Does the buffer need to outlive the current method call? If yes — don't use stackalloc, reach for pooled arrays or NativeMemory instead. (2) Is the allocation inside a loop iterating more than ~1,000 times? stackalloc inside a loop creates stack pressure that can blow the thread's guard page — I have seen this kill a server at 2,000 iterations with a 512-byte buffer. (3) Can the data be processed in place without an intermediate copy? If you're allocating just to call .ToArray() on a span, stop — use .ToArray() on the original slice instead. (4) Is there a Span<byte> constructor overload for the API you're calling? Many BCL methods now accept spans directly — Stream.Read(Span<byte>) avoids the byte[] rent-and-copy pattern entirely. (5) Are you sharing the buffer across threads? Span<T> is not thread-safe — each thread needs its own view. The tricky bit is that Memory<T> is, but only if you never hand out overlapping spans. Most teams skip this check.
One more thing: if you answered "yes" to any of these and still feel tempted to stackalloc, step back. Measure with GC.GetAllocatedBytesForCurrentThread() before and after. The numbers lie less than your intuition. We fixed this exact pattern in a telemetry pipeline last quarter — 400KB allocations per request dropped to zero, latency cut by 18%. No magic, just the checklist.
What to Do Next: Specific Actions
Profile your current hot path with BenchmarkDotNet
No guesses. No hoping. You need numbers—real ones. Grab BenchmarkDotNet and target the function you suspect is leaking allocations. I have seen teams swear their parser was allocation-free, then find 12 byte arrays per call under [MemoryDiagnoser]. Measure allocated bytes per operation, not just time. That spike on the Gen 0 column? That's your heap pressure. Run it with short-run iteration counts first—don't waste an hour polishing a benchmark for code you'll gut anyway. The catch is that single-run noise fools you; use DefaultConfig and let the framework warm up. Most teams skip this step and guess wrong.
Rewrite one function using Span and measure the difference
Pick the smallest, hottest method—a teen utility that slices strings or copies bytes. That one. Replace Substring with AsSpan().Slice(). Swap byte[] for Span<byte> on the stack where lifetime is short. Then re-run the benchmark. The tricky bit is scope: if you spill a Span out of the method as a field, you'll burn the stack and corrupt memory. Keep it local. I once rewrote a 15-line JSON parser helper this way—allocations dropped from 8 KB per call to zero, but only because the Span never escaped the loop body. Not heroic. Just surgical.
“The first rewrite feels awkward. The second feels natural. The third is when you trust the stack more than the heap.”
— paraphrased from a .NET performance discussion on the runtime repo
Read the official Span<T> documentation and constraints
Docs.microsoft.com is dry—but that dryness hides the sharp edges. You need the constraints page, the lifetime rules, and the section on ref struct limitations. Honestly—read it twice. The first pass won't stick. What usually breaks first is boxing a Span into an interface or capturing it in a lambda. The runtime will yell, but only at compile time; the team that ignores those errors ships a crash. Also check the Memory<T> vs Span<T> distinction—that trade-off (heap-safe vs. stack-optimized) matters when your API boundaries don't own the buffer. One concrete next action: bookmark docs.microsoft.com/en-us/dotnet/standard/memory-and-spans and skim the "Span usage guidelines" page before your next refactor. That page alone saves a day of debugging.
Comments (0)
Please sign in to post a comment.
Don't have an account? Create one
No comments yet. Be the first to comment!