C# 7 Series, Part 10: Span and universal memory management
Part 1: Value Tuples
Part 2: Async Main
Part 3: Default Literals
Part 4: Discards
Part 5: Private Protected
Part 6: Read-only structs
Part 7: Ref Returns
Part 8: “in” Parameters
Part 9: ref structs
Part 10: (This post) Span<T> and universal memory management
Background
.NET is a managed platform, that means the memory access and management is safe and automatic. All types are fully managed by .NET, it allocates memory either on the execution stacks, or managed heaps.
In the event of interop or low-level development, you may want the access to the native objects and system memory, here is why the interop part comes, there are types that can marshal into the native world, invoke native APIs, convert managed/native types and define a native structure from the managed code.
Problem 1: Memory access patterns
In .NET world, there are three types of memory you may be interested:
- Managed heap memory, such as an array;
- Stack memory, such as objects created by
stackalloc
; - Native memory, such as a native pointer reference.
Each type of memory access may need to use language features that are designed for it:
- To access heap memory, use the
fixed
(pinned) pointer on supported types (likestring
), or use other appropriate .NET types that have access to it, such as an array or a buffer; - To access stack memory, use pointers with
stackalloc
; - To access unmanaged system memory, use pointers with
Marshal
APIs.
You see, different access pattern needs different code, no single built-in type for all contiguous memory access.
Problem 2: Performance
In many applications, the most CPU consuming operations are string operations. If you run a profiler session against your application, you may find the fact that 95% of the CPU time is used to call string and related functions.
Trim
, IsNullOrWhiteSpace
, and SubString
may be the most frequently used string APIs, and they are also very heavy:
Trim()
orSubString()
returns a new string object that is part of the original string, this is unnecessary if there is a way to slice and return a portion of the original string to save one copy.IsNullOrWhiteSpace()
takes a string object that needs a memory copy (because string is immutable.)- Specifically, string concatenation is expensive, it takes
n
string objects, makesn
copy, generaten - 1
temporary string objects, and return a final string object, then – 1
copies can be eliminated if there is a way to get direct access to the return string memory and perform sequential writes.
Span<T>
System.Span<T>
is a stack-only type (ref struct
) that wraps all memory access patterns, it is the type for universal contiguous memory access. You can think the implementation of the Span<T> contains a dummy reference and a length, accepting all 3 memory access types.
You can create a Span<T> using its constructor overloads or implicit operators from array, stackalloc’d pointers and unmanaged pointers.
// Use implicit operator Span<char>(char[]).
Span<char> span1 = new char[] { 's', 'p', 'a', 'n' };
// Use stackalloc.
Span<byte> span2 = stackalloc byte[50];
// Use constructor.
IntPtr array = new IntPtr();
Span<int> span3 = new Span<int>(array.ToPointer(), 1);
Once you have a Span<T> object, you can set value with a specified index, or return a portion of the span:
// Create an instance.
Span<char> span = new char[] { 's', 'p', 'a', 'n' };
// Access the reference of the first element.
ref char first = ref span[0];
// Assign the reference with a new value.
first = 'S';
// You get "Span".
Console.WriteLine(span.ToArray());
// Return a new span with start index = 1 and end index = span.Length - 1.
// You get "pan".
Span<char> span2 = span.Slice(1);
Console.WriteLine(span2.ToArray());
You can then use the Slice()
method to write a high performance Trim()
method:
private static void Main(string[] args)
{
string test = " Hello, World! ";
Console.WriteLine(Trim(test.ToCharArray()).ToArray());
}
private static Span<char> Trim(Span<char> source)
{
if (source.IsEmpty)
{
return source;
}
int start = 0, end = source.Length - 1;
char startChar = source[start], endChar = source[end];
while ((start < end) && (startChar == ' ' || endChar == ' '))
{
if (startChar == ' ')
{
start++;
}
if (endChar == ' ')
{
end—;
}
startChar = source[start];
endChar = source[end];
}
return source.Slice(start, end - start + 1);
}
The above code does not copy over strings, nor generate new strings, it returns a portion of the original string by calling the Slice()
method.
Because Span<T> is a ref struct, all ref struct restrictions apply. i.e. you cannot use Span<T> in fields, properties, iterator and async methods.
Memory<T>
System.Memory<T>
is a wrapper of System.Span<T>
, make it accessible in iterator and async methods. Use the Span
property on the Memory<T> to access the underlying memory, this is extremely helpful in the asynchronous scenarios like File Streams and network communications (HttpClient
etc..)
The following code shows simple usage of this type.
private static async Task Main(string[] args)
{
Memory<byte> memory = new Memory<byte>(new byte[50]);
int count = await ReadFromUrlAsync("https://www.microsoft.com", memory).ConfigureAwait(false);
Console.WriteLine("Bytes written: {0}", count);
}
private static async ValueTask<int> ReadFromUrlAsync(string url, Memory<byte> memory)
{
using (HttpClient client = new HttpClient())
{
Stream stream = await client.GetStreamAsync(new Uri(url)).ConfigureAwait(false);
return await stream.ReadAsync(memory).ConfigureAwait(false);
}
}
The Framework Class Library/Core Framework (FCL/CoreFx) will add APIs based on the span-like types for Streams, strings and more in .NET Core 2.1.
ReadOnlySpan<T> and ReadOnlyMemory<T>
System.ReadOnlySpan<T>
is the read-only version of the System.Span<T>
struct where the indexer returns a readonly ref
object instead of ref
object. You get read-only memory access when using System.ReadOnlySpan<T>readonly ref
struct.
This is useful for string
type, because string is immutable, it is treated as read-only span.
We can rewrite the above code to implement the Trim()
method using ReadOnlySpan<T>:
private static void Main(string[] args)
{
// Implicit operator ReadOnlySpan(string).
ReadOnlySpan<char> test = " Hello, World! ";
Console.WriteLine(Trim(test).ToArray());
}
private static ReadOnlySpan<char> Trim(ReadOnlySpan<char> source)
{
if (source.IsEmpty)
{
return source;
}
int start = 0, end = source.Length - 1;
char startChar = source[start], endChar = source[end];
while ((start < end) && (startChar == ' ' || endChar == ' '))
{
if (startChar == ' ')
{
start++;
}
if (endChar == ' ')
{
end—;
}
startChar = source[start];
endChar = source[end];
}
return source.Slice(start, end - start + 1);
}
As you can see, Nothing is changed in the method body; I just changed the parameter type from Span<T>
to ReadOnlySpan<T>
, and used the implicit operator to convert a string
literal to ReadOnlySpan<char>
.
System.ReadOnlyMemory<T>
is the read-only version of System.Memory<T>
struct where the Span
property is a ReadOnlySpan<T>
. When using this type, you get read-only access to the memory and you can use it with an iterator method or async method.
Memory Extensions
The System.MemoryExtensions
class contains extension methods for different types that manipulates with span types, here is a list of commonly used extension methods, many of them are the equivalent implementations for existing APIs using the span types.
- AsSpan, AsMemory: Convert arrays into Span<T> or Memory<T> or their read-only counterparts.
- BinarySearch, IndexOf, LastIndexOf: Search elements and indexes.
- IsWhiteSpace, Trim, TrimStart, TrimEnd, ToUpper, ToUpperInvariant, ToLower, ToLowerInvariant:
Span<char>
operations similar tostring
.
Memory Marshal
In some case, you probably want to have lower level access to the memory types and system buffers, and convert between spans and read-only spans. The System.Runtime.InteropServices.MemoryMarshal
static class provides such functionalities to allow you control these access scenarios. The following code shows to title case a string using the span types, this is high performant because there is no temporary string allocations.
private static void Main(string[] args)
{
string source = "span like types are awesome!";
// source.ToMemory() converts source from string to ReadOnlyMemory<char>,
// and MemoryMarshal.AsMemory converts ReadOnlyMemory<char> to Memory<char>
// so you can modify the elements.
TitleCase(MemoryMarshal.AsMemory(source.AsMemory()));
// You get "Span like types are awesome!";
Console.WriteLine(source);
}
private static void TitleCase(Memory<char> memory)
{
if (memory.IsEmpty)
{
return;
}
ref char first = ref memory.Span[0];
if (first >= 'a' && first <= 'z')
{
first = (char)(first - 32);
}
}
Conclusion
Span<T> and Memory<T> enables a uniform way to access contiguous memory, regardless how the memory is allocated. It is very helpful for native development scenarios, as well as high performance scenarios. Especially, you will gain significant performance improvements while using span types to work with strings. It is a very nice feature innovated in C# 7.2.
NOTE: To use this feature, you will need to use Visual Studio 2017.5 and language version 7.2 or latest.
Comments
- Anonymous
August 12, 2018
Nice article. Though I can't help but wondering, in the last code snippet, you seem to have changed the actual content of a string instance. This can lead to some weird situations, especially ifsource
has already been interned (which is usually the case).Suppose we have string source = "span like types are awesome!";string source2 = "span like types are awesome!";Debug.Assert( object.ReferenceEquals( s1, s2 ) );TitleCase(MemoryMarshal.AsMemory(source.AsMemory()));Console.WriteLine(source);Console.WriteLine(source2);here. By modifying the CONTENT ofsource
string, actually you modifiedsource2
at the same time. I don't think it's a good idea somehow... - Anonymous
September 20, 2018
When I try to compile the example code for Memory section, I give me an compile error that says there's no overloaded version of ReadAsync that has only one parameter.And the next exmaple code emits compile error where you use the implicit converter. It seems that the implicit converter between string type and ReadOnlySpan type is no longer available, and you have to use the AsSpan() method in this case. - Anonymous
February 28, 2019
Saying "The above code does not copy over strings, nor generate new strings, it returns a portion of the original string by calling the Slice() method."The ToArray method IS a copy. From span to a new array.