Safe zero-copy operations in C#

ssg.dev

216 points by sedatk 2 days ago


bob1029 - 2 days ago

> Spans and slice-like structures in are the future of safe memory operations in modern programming languages. Embrace them.

I've been using Span<T> very aggressively and it makes a massive difference in cases where you need logical views into the same physical memory. All of my code has been rewritten to operate in terms of spans instead of arrays where possible.

It can be easy to overlook ToArray() or (more likely) code that implies its use in a large codebase. Even small, occasional allocations are all it takes to move your working set out of the happy place and get the GC cranking. The difference in performance can be unreasonable in some cases.

You can even do things like:

  var arena = stackalloc byte[1024];
  var segment0 = arena.Slice(10);
  var segment1 = arena.Slice(10, 200);
  ...
The above will incur no GC pressure/activity at all. Everything happens on the stack.
buybackoff - 2 days ago

For working with arrays elements without bound checks, this is the modern alternative to pointers, without object pinning for GC and "unsafe" keyword: MemoryMarshal. GetArrayDataReference<T>(T[]). This is still totally unsafe, but is "modern safer unsafe" that works with `ref`s and makes friends with System.Runtime.CompileServices.Unfafe.

Funny point: the verbosity of this method and SRCS.Unsafe ones make them look slower vs pointers at subconscious level for me, but they are as fast if not faster to juggle with knifes in C#.

The `fixed` keyword is mostly for fast transient pinning of data. Raw pointers from `fixed` remain handy in some cases, e.g. for alignment when working with AVX, but even this can be done with `ref`s, which can reference an already pinned array from Pinned Object Heap or native memory. Most APIs accept `ref`s and GC continues tracking underlying objects.

See the subtle difference here for common misuse of fixed to get array data pointer: https://sharplab.io/#v2:C4LghgzgtgPgAgJgIwFgBQcDMACR2DC2A3ut...

Spans are great, but sometimes raw `ref`s are a better fit for a task, to get the last bits of performance.

progmetaldev - 2 days ago

I truly appreciate articles like this. I am using the Umbraco CMS, and have written code to use lower than the recommended requirements to keep the entire system running. While I don't see a use for using a Span<T> yet, I could definitely see it being useful for a website with an enormous amount of content.

I am currently looking into making use of "public readonly record struct" for the models that I create for my views. Of course, I need to performance profile the code versus using standard classes with readonly properties where appropriate, but since most of my code is short-lived for pulling from the CMS to hydrate classes for the views, I'm not sure how much of a benefit I will get. Luckily I'm in a position to work on squeezing as much performance as possible between major projects.

I'm curious if anyone has found any serious performance benefit from using a Span<T> or a "public readonly record struct" in a .NET CMS, where the pages are usually fire and forget? I have spent years (since 2013) trying to squeeze every ounce of performance from the code, as I work with quite a few smaller businesses, and even the rest of my team are starting to look into Wix or Squarespace, since it doesn't require a "me" to be involved to get a site up and running.

To my credit and/or surprise, I haven't dealt with a breach to my knowledge, and I read logs and am constantly reviewing code as it is my passion (at least working within the confines of the Umbraco CMS, although it isn't my only place of knowledge). I used to work with PHP and CodeIgniter pre-2013 (then Kohana a bit while making the jump from PHP to .NET). I enjoy C#, and feel like I am able to gain quite a bit of performance from it, but if anyone has any ideas for me on how to create even more value from this, I would be extremely interested.

Freedom2 - 2 days ago

I recall a specific project involving a network appliance that generated large log streams. Our bottleneck was the log parser, which was aggressively using string.Substring() to isolate fields. This approach continuously allocated new string objects on the heap, which led to excessive pressure on the GC.

The transition to using ReadOnlySpan<char> immediately addressed the allocation issue. We were able to represent slices of the incoming buffer without any heap allocations and the parser logic was simplified significantly.

rkagerer - 2 days ago

Array element accesses are bounds-checked in C# for safety. But, that means that there's performance impact...

Why don't we have hardware support for this yet? (i.e. CPU instructions that are bounds-aware?)

Edit: Do we?

https://stackoverflow.com/questions/40752436/do-any-cpus-hav...

mwsherman - 2 days ago

I move between Go and C#. I wrote a zero-allocation package in Go [1] and then ported to C# — and the allocations exploded!

I had forgotten, or perhaps never realized, that substrings in C# allocate. The solution was Spans.

Notably, it caused me to realize that Go had “spans” designed in from the start.

[1] https://github.com/clipperhouse/uax29

hahn-kev - 2 days ago

I'm a C# dev main and love spans.

I understand it's not in the same realm as Rust, but how comparable is this to some of the power that Rust gives you?

titzer - 2 days ago

This is why Virgil has support for ranges in the language, which are better than slices. They are value types that represent a subset of a larger array. They can also be off-heap, which allows a Range<byte> to safely refer to memory-mapped buffers.

https://github.com/titzer/virgil/blob/master/doc/tutorial/Ra...

klabetron - 2 days ago

For the op, awesome article. Quick question: what’s the definition of your `swap(array, i, pivotIndex)` function? Am I missing something? Or just assumes it’s the standard set temp to a, set a to b, and set b to temp?

pjmlp - 2 days ago

This is a good example to learn how to use the tools a programming language offers, just saying a programing language has a GC thus bad is meaningless, without understanding what is actually available.

Regarding,

> Spans and slice-like structures in are the future of safe memory operations in modern programming languages.

It is sad how long stuff takes to reach mainstream technology, in Oberon the equivalent declaration to partition would be,

    PROCEDURE partition(span: ARRAY OF INTEGER): INTEGER
And if the type is the special case of ARRAY OF BYTE (need to import SYSTEM for that), then any type representation can be mapped into a span of bytes.

You will find similar capabilities in Cedar, Modula-2+, Modula-3, among several others.

Modern safe memory langaguage are finally catching up with the 1990's research, pity it always takes this much for adoption of cool ideas.

Having said this, I feel modern .NET has all the features that made me like Modula-3 back in the day, even if some are a bit convoluted like inline arrays in structs.

uecker - 2 days ago

Also works in C: https://uecker.codeberg.page/2025-07-02.html

smilekzs - 2 days ago

Anecdote: 9 years ago I was at MSFT. Hands forced by long GC pauses, eventually many teams turned to hand-rolling their flavor of string_view in C#. It was literally xkcd.com/927 back then when you tried to interface with some other team's packages and each side has the same but different string_view classes. Glad to see that finally enjoying language and stdlib support.