Dan Sorensen
Dan Sorensen

Reputation: 11753

What is the difference between Span<T> and Memory<T> in C# 7.2?

C# 7.2 introduces two new types: Span<T> and Memory<T> that have better performance over earlier C# types like string[].

Question: What is the difference between Span<T> and Memory<T>? Why would I use one over the other?

Upvotes: 103

Views: 34953

Answers (4)

Monsieur Merso
Monsieur Merso

Reputation: 2176

General

Span defined as a reference struct and Memory is defined as a struct

What does it mean?

  • Reference structs can't be stored on a heap, compiler will prevent you from doing that, so following won't be allowed:

    • Using Span as a field in a class
    • Using Span in a async method, (async methods are being expanded in a full-blown state machine)
    • and many more, here is the complete list of things that can't be done with reference structs.
  • stackalloc won't work with Memory (because there is no guarantee that it won't be stored on the heap) but will work with Span

    // this is legit
    Span<byte> data = stackalloc byte[256]; // legit
    
    // compile time error: Conversion of a stackalloc expression of type 'byte' to type 'Memory<byte>' is not possible.
    Memory<byte> data = stackalloc byte[256];
    

What does it imply?

It means that in certain scenarios various micro optimization are not possible with Span itself and therefore Memory should be used instead.

Example:

Here is an example of an string allocation free Split method, which works on ReadOnlyMemory struct, implementing this on a Span would be really difficult, due to the fact that Span is a reference struct and can't be put into array or IEnumerable:

(Implementation is taken from C# in a nutshell book)

IEnumerable<ReadOnlyMemory<char>> Split(ReadOnlyMemory<char> input)
{
    int wordStart = 0;
    for (int i = 0; i <= input.Length; i++)
    {
        if (i == input.Length || char.IsWhiteSpace(input.Span[i]))
        {
            yield return input.Slice(wordStart, i - wordStart);
            wordStart = i + 1;
        }
    }
}

And here are results of a very simple benchmark via dotnet benchmark library against regular Split method, on a .NET SDK=6.0.403.

|                Method |     StringUnderTest |              Mean |             Error |            StdDev |      Gen0 |      Gen1 |     Gen2 |  Allocated |
|---------------------- |-------------------- |------------------:|------------------:|------------------:|----------:|----------:|---------:|-----------:|
|          RegularSplit |                meow |         13.194 ns |         0.2891 ns |         0.3656 ns |    0.0051 |         - |        - |       32 B |
| SplitOnReadOnlyMemory |                meow |          8.991 ns |         0.1981 ns |         0.2433 ns |    0.0127 |         - |        - |       80 B |
|          RegularSplit | meow(...)meow [499] |      1,077.807 ns |        21.2291 ns |        34.8801 ns |    0.6409 |    0.0095 |        - |     4024 B |
| SplitOnReadOnlyMemory | meow(...)meow [499] |          9.036 ns |         0.2055 ns |         0.2366 ns |    0.0127 |         - |        - |       80 B |
|          RegularSplit | meo(...)eow [49999] |    121,740.719 ns |     2,221.3079 ns |     2,077.8128 ns |   63.4766 |   18.5547 |        - |   400024 B |
| SplitOnReadOnlyMemory | meo(...)eow [49999] |          9.048 ns |         0.2033 ns |         0.2782 ns |    0.0127 |         - |        - |       80 B |
|          RegularSplit | me(...)ow [4999999] | 67,502,918.403 ns | 1,252,689.2949 ns | 2,092,962.4006 ns | 5625.0000 | 2375.0000 | 750.0000 | 40000642 B |
| SplitOnReadOnlyMemory | me(...)ow [4999999] |          9.160 ns |         0.2057 ns |         0.2286 ns |    0.0127 |         - |        - |       80 B |

Inputs for those methods were, strings of "meow " repeated 1, 100 , 10_000 and 1_000_000 times, my benchmark setup wasn't ideal but it shows the difference.

Upvotes: 10

Olivier Duhart
Olivier Duhart

Reputation: 447

Memory<T> can be seen as an unsafe but more versatile version of Span<T>. Access to a Memory<T> object will fail if it points to a freed array.

Upvotes: -11

Krzysztof Cwalina
Krzysztof Cwalina

Reputation: 489

re: this means it can only point to memory allocated on the stack.

Span<T> can point to any memory: allocated either on the stack or the heap. Stack-only nature of Span<T> means that the Span<T> itself (not the memory it points to) must reside only on the stack. This is in contrast to "normal" C# structs, that can reside on the stack or on the heap (either via value-type boxing, or when they are embedded in classes/reference-types). Some of the more obvious practical implications are that you cannot have a Span<T> field in a class, you cannot box Span<T>, and you cannot make an array of them.

Upvotes: 48

Hussein Salman
Hussein Salman

Reputation: 8246

Span<T> is stack-only in nature while Memory<T> can exist on the heap.

Span<T> is a new type we are adding to the platform to represent contiguous regions of arbitrary memory, with performance characteristics on par with T[]. Its APIs are similar to the array, but unlike arrays, it can point to either managed or native memory, or to memory allocated on the stack.

Memory <T> is a type complementing Span<T>. As discussed in its design document, Span<T> is a stack-only type. The stack-only nature of Span<T> makes it unsuitable for many scenarios that require storing references to buffers (represented with Span<T>) on the heap, e.g. for routines doing asynchronous calls.

async Task DoSomethingAsync(Span<byte> buffer) {
    buffer[0] = 0;
    await Something(); // Oops! The stack unwinds here, but the buffer below
                       // cannot survive the continuation.
    buffer[0] = 1;
}

To address this problem, we will provide a set of complementary types, intended to be used as general purpose exchange types representing, just like Span <T>, a range of arbitrary memory, but unlike Span <T> these types will not be stack-only, at the cost of significant performance penalties for reading and writing to the memory.

async Task DoSomethingAsync(Memory<byte> buffer) {
    buffer.Span[0] = 0;
    await Something(); // The stack unwinds here, but it's OK as Memory<T> is
                       // just like any other type.
    buffer.Span[0] = 1;
}

In the sample above, the Memory <byte> is used to represent the buffer. It is a regular type and can be used in methods doing asynchronous calls. Its Span property returns Span<byte>, but the returned value does not get stored on the heap during asynchronous calls, but rather new values are produced from the Memory<T> value. In a sense, Memory<T> is a factory of Span<T>.

Reference Document: here

Upvotes: 92

Related Questions