Skip to content
Ian Griffiths By Ian Griffiths Technical Fellow I
C#, Span and async

C# 7.2 introduced support for ref struct types. These are types that are required to live on the stack. The most notable ref struct types are Span<T> and ReadOnlySpan<T> which have made it possible to dramatically improve the efficiency of certain kinds of work by reducing or even eliminating object allocations.

Endjin recently open sourced our AIS parser at https://github.com/ais-dotnet/ and it relies on these techniques to make it possible to parse millions of messages a second on a single thread, all without any per-message memory allocation. (AIS is the international standard by which ships report information including location, speed, and heading. We wrote this parser as part of a recent project with OceanMind. This video provides information about the kind of work OceanMind does.)

There's a tradeoff with these techniques: ref struct types can only live on the stack. They rely on the .NET runtime's support for managed interior pointers. These are special kinds of pointers that are allowed to point into the middle of an object—at a particular field in an object or a particular element in an array—and if any of these pointers are live, they will keep their target object alive just like a normal object reference would. (A normal object reference always points to the start of the object.) But they are more complex for the GC to manage, so it only supports managed pointers that live on the stack.

This stack-only requirement obviously means you can't write a class with instance fields of any ref struct type—a class's instance fields live inside the object's memory on the managed heap. More subtly you can't use these types for fields in an ordinary struct either—although value types often live on the stack, they don't have to: if you use a value type as a field in a class, or an element type of an array, the values will end up in a heap block. (And of course, if you box a value type, it ends up being copied into its very own heap block.)

The only types that can have fields with types such as ReadOnlySpan<T> are other ref struct types.

Iterators, async, and ref structs

A problem you're highly likely to run into if you use ref struct types much is that the stack-only limitations come into play whenever C# turns your local variables into fields. It does this with any async method for example. (And also for iterator methods, and for the same reason: both kinds need local variables to be able to survive after the method returns, because these methods can typically return several times during the course of their execution, later picking up again from where they left off.)

The upshot is that you can't declare a ref struct-typed local variable in an async method or an iterator. That means that this won't work:

Programming C# 10 Book, by Ian Griffiths, published by O'Reilly Media, is now available to buy.

This tries to use the NmeaAisPositionReportClassAParser from our Ais.Net library. That type uses ReadOnlySpan<T> internally as a field, meaning that it too has to be a ref struct. So you can't declare a variable of this type in an async method.

However, it's relatively straightforward to work around this, using the technique shown here:

The Introduction to Rx.NET 2nd Edition (2024) Book, by Ian Griffiths & Lee Campbell, is now available to download for FREE.

I've moved the code that uses the ref struct type into a local method. That local method is not async meaning that you can't use await inside it, but you can have ref struct variables.

Although C# has to generate a hidden type to give the local method access to variable declared by the outer method (e.g., line in this example), this doesn't cause any extra allocations. The hidden type will be a struct in this case, and it will be passed to the local method as a hidden ref argument, avoiding either unnecessary copying or object allocation. (Be aware that if you create a delegate that refers to a local method, C# is no longer able to perform that optimization, because it needs to ensure that the local variables are able to outlive the outer method, just in case you try to use the delegate after returning. But for this scenario, that problem does not arise.)

This technique also works for iterator methods. And of course it works for .NET's own ref struct types such as Span<T>.

Ian Griffiths

Technical Fellow I

Ian Griffiths

Ian has worked in various aspects of computing, including computer networking, embedded real-time systems, broadcast television systems, medical imaging, and all forms of cloud computing. Ian is a Technical Fellow at endjin, and Microsoft MVP in Developer Technologies. He is the author of O'Reilly's Programming C# 10.0, and has written Pluralsight courses on WPF (and here) and the TPL. He's a maintainer of Reactive Extensions for .NET, Reaqtor, and endjin's 50+ open source projects. Technology brings him joy.