Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

C# & .NET (C# 14, .NET 10 LTS)  Module 17 – Memory, Span and performance

Span<T>, ReadOnlySpan<T> and Memory<T>

Slice arrays and strings without copying with Span<T> and ReadOnlySpan<T>, keep a buffer across await with Memory<T>, and parse text without allocating.

  • Advanced
  • 15 minutes
  • Examples run with .NET 10.0.401 and Python 3.14.8
  • By MySmartCoPilot
Free core, Pro depth: 5 of 15 minutes free
Free: A span views memory it does not own, and copies nothing · Memory&lt;T&gt; is the view you can keep across await. With a pass: 3 in-depth parts and 3 interview answers, about 10 minutes.

What you will learn

  • Slice arrays and strings without copying them, and write through a span
  • Choose between Span<T> and Memory<T> for code that stores a buffer or awaits
  • Parse text records without allocating, and reject malformed records
  • Read and fix the compiler errors that keep spans safe, and the overload changes of C# 14

Before you start

What you’ll learn · Before you start

What you will learn

  • Slice arrays and strings without copying them, and write through a span
  • Choose between Span<T> and Memory<T> for code that stores a buffer or awaits
  • Parse text records without allocating, and reject malformed records
  • Read and fix the compiler errors that keep spans safe, and the overload changes of C# 14

Before you start

On this page

Reading the age out of 10,000 records such as “Asha,18,Pune” with string.Split allocated 150.1 bytes a record: an array and three new strings for every record, made only to be thrown away. Reading the same records through a span: A Span<T> or ReadOnlySpan<T>: a view of a run of values that sit next to each other in memory, such as part of an array, which reads and writes them where they are. array.AsSpan(1, 3) refers to three elements of array and copies nothing. More in “Arrays, indices and ranges” allocated nothing at all.

A span is a view of memory that belongs to something else: part of an array, a string or the stack. This lesson shows the two span types, Span<T> and ReadOnlySpan<T>, which read and write such views in place, and Memory<T>: A struct that views a run of values in an array, a string or a pooled buffer, like a span, but can be stored in fields and kept across await. Its Span property gives a span for synchronous work., the view that code can keep in a field and use across await. You will slice without copying, pick the right type for code that waits, and parse text without allocating.

A span views memory it does not own, and copies nothing

views.cs slices an array, writes through the slice, then adds up the ages of 10,000 records in two ways:

views.cs: an array slice, two parsers, and stack memory C# · views/views.cs
// A span is a view: slicing copies nothing, writing through a span changes the memory it views, and a string can be
// read through a span without making new strings.
int[] marks = [41, 67, 88, 52, 73, 90, 35, 64];
Span<int> middle = marks.AsSpan(2, 4); // positions 2 to 5 of marks, not a copy
middle[0] = 99;                        // writes marks[2]
Console.WriteLine($"marks:  {string.Join(", ", marks)}");
Console.WriteLine($"middle: {middle.Length} values from position 2, first {middle[0]}, last {middle[^1]}");

string[] records = MakeRecords(10_000);
Console.WriteLine($"with Split: {BytesPerRecord(SumAgesWithSplit, records),5:F1} bytes a record, total {SumAgesWithSplit(records)}");
Console.WriteLine($"with spans: {BytesPerRecord(SumAgesWithSpans, records),5:F1} bytes a record, total {SumAgesWithSpans(records)}");

Span<char> digits = stackalloc char[11]; // a span over the stack: no heap at all
bool written = 41_250.TryFormat(digits, out int length);
Console.WriteLine($"stackalloc: {written}, {length} characters: {digits[..length]}");

// Adds up the ages of the records, reading each one with string.Split.
static long SumAgesWithSplit(string[] records)
{
    long total = 0;
    foreach (string record in records)
    {
        string[] fields = record.Split(','); // a new array and three new strings
        total += int.Parse(fields[1]);
    }
    return total;
}

// The same, reading each record through a ReadOnlySpan<char>.
static long SumAgesWithSpans(string[] records)
{
    long total = 0;
    foreach (string record in records)
    {
        ReadOnlySpan<char> line = record;
        ReadOnlySpan<char> rest = line[(line.IndexOf(',') + 1)..]; // after the name
        total += int.Parse(rest[..rest.IndexOf(',')]);             // up to the city
    }
    return total;
}

static double BytesPerRecord(Func<string[], long> sumAges, string[] records)
{
    sumAges(records); // warm up first, so that only the work itself is counted
    long before = GC.GetAllocatedBytesForCurrentThread();
    sumAges(records);
    return (GC.GetAllocatedBytesForCurrentThread() - before) / (double)records.Length;
}

static string[] MakeRecords(int count)
{
    string[] names = ["Asha", "Ravi", "Meera", "Kabir", "Ishaan", "Zoya"];
    string[] cities = ["Pune", "Kochi", "Indore", "Mysuru", "Guwahati"];
    var records = new string[count];
    for (int i = 0; i < count; i++) records[i] = $"{names[i % names.Length]},{18 + i % 50},{cities[i % cities.Length]}";
    return records;
}

Output

marks:  41, 67, 99, 52, 73, 90, 35, 64
middle: 4 values from position 2, first 99, last 90
with Split: 150.1 bytes a record, total 425000
with spans:   0.0 bytes a record, total 425000
stackalloc: True, 5 characters: 41250

Recorded with .NET SDK 10.0.401 (runtime 10.0.12) on macOS 26 arm64. To run it yourself: dotnet run views.cs

marks.AsSpan(2, 4) made no new array. The span holds a reference to marks[2] and a length of 4, so writing middle[0] wrote marks[2], which now holds 99. That is all a Span<T> is: a view of values that sit next to each other in memory, such as an array, part of one or memory from stackalloc, read and written in place. A ReadOnlySpan<T> is the same view without writing, and it is what a string gives you, because strings never change. Step through the program:

Interactive · message by message What a span is while views.cs runs Step through views.cs. The program is on the left, the span in the middle and the memory it views on the right; a message is a view handed over or a value read or written through it.
What a span is while views.cs runs, step 6 of 6. Messages so far: views.cs to the span: AsSpan(2, 4); the span to the memory: write 99; views.cs to the span: line = record; views.cs to the span: slice after the comma; the span to views.cs: int.Parse gives 18; views.cs to the memory: stackalloc char[11].views.csthe programthe spanstack memory, length 11the memory11 chars on the stackAsSpan(2, 4)write 99line = recordslice after the commaint.Parse gives 18stackalloc char[11]

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. A slice of the array. marks.AsSpan(2, 4) copies nothing. The span stores where position 2 of marks is and that it covers 4 values; the 8 marks stay in their array.
  2. Writing through it. middle[0] = 99 writes into the memory the span views: position 0 of the span is position 2 of marks, so the array now holds 99 there.
  3. A view of a string. ReadOnlySpan<char> line = record makes a read-only view of the first record, "Asha,18,Pune", 12 characters long. It cannot write, because strings never change, and it allocates nothing.
  4. A slice of the view. line[(line.IndexOf(',') + 1)..] is another view of the same characters, starting after the first comma: "18,Pune", 7 characters. Still nothing is copied.
  5. Parsing without a string. int.Parse has an overload that reads a ReadOnlySpan<char>, so the age is read straight from the record: the slice "18" goes in, the number 18 comes out, and no string is made.
  6. A view of the stack. stackalloc char[11] gives a span over 11 characters on the stack, where TryFormat writes 41250. That memory is gone when the method returns, and the compiler makes sure no span outlives it.

In the parse loop, Split made an array and three strings for every record, 150.1 bytes, while the span version found the commas with IndexOf, sliced, and gave the slice to int.Parse, which has an overload for spans: 0.0 bytes, for the same total.

Turning every slice back into a string

rest[..comma].ToString() copies the slice into a new string, and a parser that does it for every field allocates as much as Split did. Keep fields as spans and give them to the span overloads of int.Parse, double.Parse and SequenceEqual; make a string only for a value you keep.

Checkpoint

Check: A span as a view of memory

  1. Question 1 of 2 int[] a = [10, 20, 30, 40, 50]; then Span<int> s = a.AsSpan(1, 3); and s[2] = 7;. Which element of a holds 7 now?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: a[3]

    A span is a reference to its first element plus a length. Index i of the span is index start + i of the array: 1 + 2 = 3.

  2. Question 2 of 2 In views.cs, reading a record with Split cost 150.1 bytes. How many MB does reading 1,000,000 such records with Split allocate? (1 MB = 1,000,000 bytes)

    Type a number.

    A hint

    Multiply the bytes per record by the number of records.

    Show the answer: Question 2 of 2

    Answer: 150.1 MB (anything from 150 to 150.2 counts)

    150.1 bytes × 1,000,000 = 150,100,000 bytes, about 150 MB of short-lived garbage; the span version allocated 0.0 bytes a record for the same total.

Memory<T> is the view you can keep across await

A span cannot leave the stack. Span<T> is a ref struct: A struct declared with ref struct, such as Span<T>. Its instances live only on the stack: it cannot be boxed, be a field of a class, be captured by a lambda or be kept across an await. More in “ref structs, ref fields and scoped”, so a class cannot have a span field, and an async method cannot keep a span across an await, because its locals may move to the heap while it waits. Memory<T> and ReadOnlyMemory<T> view the same arrays and strings, or a buffer rented from a pool, but are ordinary structs, which can be stored anywhere; their Span property gives a span for the synchronous work in between. memory.cs keeps a read-only memory in a field and returns one line per await:

memory.cs: a line reader that keeps its text across await C# · memory/memory.cs
// Memory<T> can be kept in a field and used across await; .Span gives a span for the synchronous work in between.
var reader = new LineReader("Asha,34,Pune\nRavi,41,Kochi\nMeera,29,Indore\n".AsMemory());
int count = 0;
while (await reader.NextAsync() is ReadOnlyMemory<char> line)
    Console.WriteLine($"line {++count}: {line.Length} characters, \"{line}\"");
Console.WriteLine($"{reader.Remaining} characters left");

sealed class LineReader(ReadOnlyMemory<char> text)
{
    private ReadOnlyMemory<char> _rest = text; // a field of a class: allowed for Memory<T>, refused for Span<T>

    public int Remaining => _rest.Length;

    public async Task<ReadOnlyMemory<char>?> NextAsync()
    {
        await Task.Yield(); // stands in for waiting on the network
        if (_rest.IsEmpty) return null;
        int end = _rest.Span.IndexOf('\n'); // a span for the synchronous work, between two awaits
        ReadOnlyMemory<char> line = _rest[..end];
        _rest = _rest[(end + 1)..];
        return line;
    }
}

Output

line 1: 12 characters, "Asha,34,Pune"
line 2: 13 characters, "Ravi,41,Kochi"
line 3: 15 characters, "Meera,29,Indore"
0 characters left

Recorded with .NET SDK 10.0.401 (runtime 10.0.12) on macOS 26 arm64. To run it yourself: dotnet run memory.cs

Each call awaited first, then took _rest.Span to find the next newline, and sliced the memory itself for the line it returned and the rest it kept. The three lines and the field all point into the one string the reader was given.

Span<T>, ReadOnlySpan<T> Memory<T>, ReadOnlyMemory<T>
Kind of type a ref struct, stack only a struct that lives anywhere
A field of a class refused allowed
Across await or in a lambda refused allowed
A view of stack memory yes no
Reading an element index it take .Span first
Use it for synchronous work buffers kept or awaited

Microsoft’s usage guidelines turn the table into two rules for APIs: a synchronous method takes a Span<T>, because any caller that holds a Memory<T> can pass its .Span, and a method that only reads takes the read-only type. For parsing, int.Parse reads a ReadOnlySpan<char> directly. MemoryExtensions.Split finds the fields of a span without making strings. SearchValues prepares a set of characters once for fast searches. C# 14 converts arrays to spans in more places, so more span methods can be called on arrays directly. Code that keeps a Memory<T> must also respect how long it may use it, which the guidelines call a lease.

Checkpoint

Check: Span or Memory for code that keeps or awaits

  1. Question 1 of 2 A class must keep the text it reads from between calls to its methods. Which type can that field have?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: ReadOnlyMemory<char>

    Spans live on the stack only. To keep a view in an object, store a Memory or ReadOnlyMemory and take its Span when you work on it.

  2. Question 2 of 2 An async method reads a name out of a line, awaits a database call, then writes the name into the result. What should it keep across the await?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: A string or a ReadOnlyMemory<char> of the name, not a span

    An await may move the method's locals to the heap, so nothing that crosses an await may be a span. Keep a string or a memory slice of the name instead.

Parsing records with spans, measured to the byte Pro: opens with a Pro, Premium or Ultimate pass

Real records have several fields, delimiters that vary and mistakes in them. This part parses them with the span APIs of .NET 8 and .NET 9, measures each way to the byte, and ends with a parser for sensor records that you finish.

In this part:

  • Splitting a record into ranges, without one new string
  • Searching for any of many characters with SearchValues
  • A sensor record parser that allocates nothing

3 runnable examples (C#) · about 4 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

What the compiler refuses, and what C# 14 converts for you Pro: opens with a Pro, Premium or Ultimate pass

The rules that keep spans on the stack are checked when the code compiles, never at run time. This part reproduces the two errors met most often, fixes them, and shows C# 14’s new conversions with the one overload change that can fail at run time.

In this part:

  • CS8175 and CS4007, reproduced and fixed
  • C# 14 turns arrays into spans for you
  • An overload that C# 14 picks differently

6 runnable examples (C#, Python) · about 4 minutes

Opening this part with your pass…Pro part. It opens with the same pass as the part above.

Who may use a Memory buffer across async code, and until when Pro: opens with a Pro, Premium or Ultimate pass

A Memory<T> can be kept, which raises the question of for how long. This part states the ownership rules of Microsoft’s usage guidelines, breaks one of them on purpose and fixes it, so you can spot the bug in a code review.

In this part:

  • The lease: what a method may do with a Memory<T> it was given
  • The rules, and the owner types that make them explicit

1 runnable example (C#) · about 4 minutes

Opening this part with your pass…Pro part. It opens with the same pass as the part above.

Practise

Check yourself on views and slicing and on choosing between a span and Memory<T>. The session picks the next question from your earlier answers and explains each choice as soon as you make it:

Practice that adapts to you

Practise this lesson

6 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 6 What does a Span<T> hold?

    Choose one answer.

    Show the answer: Question 1 of 6

    Answer: A reference to its first element and a length

    A span is a reference plus a length: a view of a run of values in an array, a string, stack or native memory.

  2. Question 2 of 6 string s = "ID-42"; ReadOnlySpan<char> number = s.AsSpan(3); What does number.Length return, and what was allocated?

    Choose one answer.

    Show the answer: Question 2 of 6

    Answer: 2, and nothing was allocated

    AsSpan(start) gives a read-only view from start to the end of the string; strings never change, so the view is always read-only.

  3. Question 3 of 6 A parser reads 20 fields per line through spans but calls ToString() on each field before parsing it. What does that do to its allocations?

    Choose one answer.

    Show the answer: Question 3 of 6

    Answer: It brings them back: one new string per field, as many as Split would make

    Spans save allocations only while the values stay spans. Make a string only for a value you keep.

  4. Question 4 of 6 Which of these may be the type of a field in a class?

    Choose one answer.

    Show the answer: Question 4 of 6

    Answer: Memory<byte>

    The ref struct types Span and ReadOnlySpan stay on the stack; the Memory types were made to be stored.

  5. Question 5 of 6 A method holds a ReadOnlyMemory<char> named text and needs the position of the first comma in it. How does it search?

    Choose one answer.

    Show the answer: Question 5 of 6

    Answer: text.Span.IndexOf(',')

    Memory is for keeping; Span is for working. Take .Span, do the synchronous work, and keep only Memory or values.

  6. Question 6 of 6 You design a synchronous method that computes a checksum of some bytes and keeps nothing. What should its parameter be?

    Choose one answer.

    Show the answer: Question 6 of 6

    Answer: ReadOnlySpan<byte>

    Take a span in a synchronous method and the read-only span when it does not write: arrays, slices, stack memory and Memory.Span all convert to it.

Interview questions

Fresher · What is the difference between Span<T> and Memory<T>?

Both are views of a run of values that sit next to each other in memory, such as part of an array or a string, and neither copies what it views. Span<T> is a ref struct: it can live only on the stack, so it cannot be a field of a class, be captured by a lambda or be kept across an await, but it can also view stack memory from stackalloc, and it is the fastest to use. Memory<T> is an ordinary struct that can be stored anywhere, so it suits buffers that async code or an object keeps; to work on its values you take its Span property. A synchronous method should take a span, and code that stores a buffer or awaits should take a Memory<T>.

  1. Mid to senior

    A CSV import service parses 2 million lines a minute with string.Split and int.Parse, and an allocation profile shows that most of its garbage comes from parsing. How would you rewrite the parser with spans, and what would you measure before and after?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    A developer turns a synchronous parsing method into an async one that awaits a lookup between two steps, and the build fails with CS4007 on a ReadOnlySpan<char> local. Explain the error and two ways to fix it, with what each costs.

    Model answer, Pro. First step and follow-ups

  3. Senior to staff

    After a library moves to C# 14, a call that passes an array to one of its overloaded methods picks a different overload, and one caller's test now throws ArrayTypeMismatchException. Explain what changed and how you fix the call and the library.

    Model answer, Pro. First step and follow-ups

Key takeaways

  • A span is a view: slicing an array or a string with a span copies nothing, and writing through a Span<T> writes the memory it views.
  • Parsing through ReadOnlySpan<char> with the span overloads of int.Parse and the like reads text without making strings, 0 bytes a record against 150.1 with Split.
  • Span<T> is a ref struct that stays on the stack; Memory<T> can be kept in fields and across await, and its Span property gives a span for the synchronous work.

Terms in this lesson

Memory<T>
A struct that views a run of values in an array, a string or a pooled buffer, like a span, but can be stored in fields and kept across await. Its Span property gives a span for synchronous work.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.