译注:这是本系列最后一篇文章api
背景数组
.NET是一个托管平台,这意味着内存访问和管理是安全的、自动的。全部类型都是由.NET彻底管理的,它在执行栈或托管堆上分配内存。安全
在互操做的事件或低级别开发中,你可能但愿访问本机对象和系统内存,这就是为何会有互操做这部分了,有一部分类型能够封送进入本机世界,调用本机api,转换托管/本机类型和在托管代码中定义一个本机结构。网络
问题1:内存访问模式框架
在.NET世界中,你可能会对3种内存类型感兴趣。异步
- 托管堆内存,如数组;
- 栈内存,如使用stackalloc建立的对象;
- 本机内存,例如本机指针引用。
上面每种类型的内存访问可能须要使用为它设计的语言特性:async
- 要访问堆内存,请在支持的类型(如字符串)上使用fixed(固定)指针,或者使用其余能够访问它的适当.NET类型,如数组或缓冲区;
- 要访问堆栈内存,请使用stackalloc建立指针;
- 要访问非托管系统内存,请使用Marshal api建立指针。
你看,不一样的访问模式须要不一样的代码,对于全部连续的内存访问没有单一的内置类型。函数
问题2:性能性能
在许多应用程序中,最消耗CPU的操做是字符串操做。若是你对你的应用程序运行一个分析器会话,你可能会发现95%的CPU时间都用于调用字符串和相关函数。
Trim、IsNullOrWhiteSpace和SubString多是最经常使用的字符串api,它们也很重:
- Trim()或SubString()返回一个新的字符串对象,该对象是原始字符串的一部分,若是有办法切片并返回原始字符串的一部分来保存一个副本,其实没有必要这样作。
- IsNullOrWhiteSpace()获取一个须要内存拷贝的字符串对象(由于字符串是不可变的)。
- 特别的,字符串链接很昂贵(译注:指消耗不少CPU),须要n个字符串对象,产生n个副本,生成n-1个临时字符串对象,并返回一个字符串对象,那n-1个副本本能够排除的若是有办法直接访问返回字符串内存和执行顺序写入。
Span<T>
System.Span<T>是一个只在栈上的类型(ref struct),它封装了全部的内存访问模式,它是一种用于通用连续内存访问的类型。你能够认为Span<T>的实现包含一个虚拟引用和一个长度,接受所有3种内存访问类型。
你可使用Span<T>的构造函数重载或来自数组、stackalloc的指针和非托管指针的隐式操做符来建立Span<T>。
// 使用隐式操做 Span<char>(char[])。 Span<char> span1 = new char[] { 's', 'p', 'a', 'n' }; // 使用stackalloc。 Span<byte> span2 = stackalloc byte[50]; // 使用构造函数。 IntPtr array = new IntPtr(); Span<int> span3 = new Span<int>(array.ToPointer(), 1);
一旦你有了一个Span<T>对象,你能够用指定的索引来设置值,或者返回Span的一部分:
// 建立一个实例: Span<char> span = new char[] { 's', 'p', 'a', 'n' }; // 访问第一个元素的引用。 ref char first = ref span[0]; // 给引用设置一个新的值。 first = 'S'; // 新的字符串"Span". Console.WriteLine(span.ToArray());
// 返回一个新的span从索引1到末尾. // 获得"pan"。 Span<char> span2 = span.Slice(1); Console.WriteLine(span2.ToArray());
你可使用Slice()方法编写一个高性能Trim()方法:
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); }
上面的代码不复制字符串,也不生成新的字符串,它经过调用Slice()方法返回原始字符串的一部分。
由于Span<T>是一个ref结构,因此全部的ref结构限制都适用。也就是说,你不能在字段、属性、迭代器和异步方法中使用Span<T>。
Memory<T>
System.Memory<T>是一个System.Span<T>的包装。使其在迭代器和异步方法中可访问。使用Memory<T>上的Span属性来访问底层内存,这在异步场景中很是有用,好比文件流和网络通讯(HttpClient等)。
下面的代码展现了这种类型的简单用法。
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); } }
框架类库/核心框架(FCL/CoreFx)将在.NET Core 2.1中为流、字符串等添加基于类Span类型的api。
ReadOnlySpan<T> 和 ReadOnlyMemory<T>
System.ReadOnlySpan<T>是System.Span<T>的只读版本。其中,索引器返回一个只读的ref对象,而不是ref对象。在使用System.ReadOnlySpan<T>这个只读的ref结构时,你能够得到只读的内存访问权限。
这对于string类型很是有用,由于string是不可变的,因此它被视为只读的span。
咱们能够重写上面的代码来实现Trim()方法,使用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); }
如你所见,方法体中没有任何更改;我只是将参数类型从Span<T>更改成ReadOnlySpan<T>,并使用隐式操做符将字符串直接转换为ReadOnlySpan<char>。
Memory扩展方法
System.MemoryExtensions类包含针对不一样类型的扩展方法,这些方法使用span类型进行操做,下面是经常使用的扩展方法列表,其中许可能是使用span类型的现有api的等效实现。
- AsSpan, AsMemory:将数组转换成Span<T>或Memory<T>或它们的只读副本。
- BinarySearch, IndexOf, LastIndexOf:搜索元素和索引。
- IsWhiteSpace, Trim, TrimStart, TrimEnd, ToUpper, ToUpperInvariant, ToLower, ToLowerInvariant:相似字符串的Span<char>操做。
内存封送
在某些状况下,你可能但愿对内存类型和系统缓冲区有较低级别的访问权限,并在span和只读span之间进行转换。System.Runtime.InteropServices.MemoryMarshal静态类提供了此类功能,容许你控制这些访问场景。下面的代码展现了使用span类型来作首字母大写,这个实现性能高,由于没有临时的字符串分配。
private static void Main(string[] args) { string source = "span like types are awesome!"; // source.ToMemory() 转换变量 source 从字符串类型为 ReadOnlyMemory<char>, // and MemoryMarshal.AsMemory 转换 ReadOnlyMemory<char> 为 Memory<char> // 这样你就能够修改元素了。 TitleCase(MemoryMarshal.AsMemory(source.AsMemory())); // 获得 "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); } }
结论
Span<T>和Memory<T>支持以统一的方式访问连续内存,而无论内存是如何分配的。它对本地开发场景以及高性能场景很是有帮助。特别是,在使用span类型处理字符串时,你将得到显著的性能改进。这是C# 7.2中一个很是好的创新特性。
注意:要使用此功能,你须要使用Visual Studio 2017.5和C#语言版本7.2或最新版本。
系列文章:
- [译]C# 7系列,Part 1: Value Tuples 值元组
- [译]C# 7系列,Part 2: Async Main 异步Main方法
- [译]C# 7系列,Part 3: Default Literals 默认文本表达式
- [译]C# 7系列,Part 4: Discards 弃元
- [译]C# 7系列,Part 5: private protected 访问修饰符
- [译]C# 7系列,Part 6: Read-only structs 只读结构
- [译]C# 7系列,Part 7: ref Returns ref返回结果
- [译]C# 7系列,Part 8: in Parameters in参数
- [译]C# 7系列,Part 9: ref structs ref结构
- [译]C# 7系列,Part 10: Span<T> and universal memory management Span<T>和统一内存管理 (本文,完)