[译]C# 7系列,Part 10: Span<T> and universal memory management Span<T>和统一内存管理
译注:这是本系列最后一篇文章
背景
.NET是一个托管平台,这意味着内存访问和管理是安全的、自动的。所有类型都是由.NET完全管理的,它在执行栈或托管堆上分配内存。
在互操作的事件或低级别开发中,你可能希望访问本机对象和系统内存,这就是为什么会有互操作这部分了,有一部分类型可以封送进入本机世界,调用本机api,转换托管/本机类型和在托管代码中定义一个本机结构。
问题1:内存访问模式
在.NET世界中,你可能会对3种内存类型感兴趣。
- 托管堆内存,如数组;
- 栈内存,如使用stackalloc创建的对象;
- 本机内存,例如本机指针引用。
上面每种类型的内存访问可能需要使用为它设计的语言特性:
- 要访问堆内存,请在支持的类型(如字符串)上使用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[]; // 使用构造函数。
IntPtr array = new IntPtr();
Span<int> span3 = new Span<int>(array.ToPointer(), );
一旦你有了一个Span<T>对象,你可以用指定的索引来设置值,或者返回Span的一部分:
// 创建一个实例:
Span<char> span = new char[] { 's', 'p', 'a', 'n' };
// 访问第一个元素的引用。
ref char first = ref span[];
// 给引用设置一个新的值。
first = 'S';
// 新的字符串"Span".
Console.WriteLine(span.ToArray());
// 返回一个新的span从索引1到末尾.
// 得到"pan"。
Span<char> span2 = span.Slice();
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 = , end = source.Length - ;
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 + );
}
上面的代码不复制字符串,也不生成新的字符串,它通过调用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[]);
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 = , end = source.Length - ;
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 + );
}
如你所见,方法体中没有任何更改;我只是将参数类型从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[];
if (first >= 'a' && first <= 'z')
{
first = (char)(first - );
}
}
结论
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>和统一内存管理 (本文,完)
[译]C# 7系列,Part 10: Span<T> and universal memory management Span<T>和统一内存管理的更多相关文章
- [译]C# 7系列,Part 9: ref structs ref结构
原文:https://blogs.msdn.microsoft.com/mazhou/2018/03/02/c-7-series-part-9-ref-structs/ 背景 在之前的文章中,我解释了 ...
- [译]C# 7系列,Part 8: in Parameters in参数
原文:https://blogs.msdn.microsoft.com/mazhou/2018/01/08/c-7-series-part-8-in-parameters/ 背景 默认情况下,方法参数 ...
- [译]C# 7系列,Part 1: Value Tuples 值元组
Mark Zhou写了很不错的一系列介绍C# 7的文章,虽然是2年多年前发布的,不过对于不熟悉C# 7特性的同学来说,仍然有很高的阅读价值. 原文:https://blogs.msdn.microso ...
- [译]C# 7系列,Part 2: Async Main 异步Main方法
原文:https://blogs.msdn.microsoft.com/mazhou/2017/05/30/c-7-series-part-2-async-main/ 你大概知道,C#语言可以构建两种 ...
- [译]C# 7系列,Part 3: Default Literals 默认文本表达式
原文:https://blogs.msdn.microsoft.com/mazhou/2017/06/06/c-7-series-part-3-default-literals/ C#的default ...
- [译]C# 7系列,Part 4: Discards 弃元
原文:https://blogs.msdn.microsoft.com/mazhou/2017/06/27/c-7-series-part-4-discards/ 有时我们想要忽略一个方法返回的值,特 ...
- [译]C# 7系列,Part 5: private protected 访问修饰符
原文:https://blogs.msdn.microsoft.com/mazhou/2017/10/05/c-7-series-part-5-private-protected/ C#有几个可访问性 ...
- [译]C# 7系列,Part 6: Read-only structs 只读结构
原文:https://blogs.msdn.microsoft.com/mazhou/2017/11/21/c-7-series-part-6-read-only-structs/ 背景 在.NET世 ...
- [译]C# 7系列,Part 7: ref Returns ref返回结果
原文:https://blogs.msdn.microsoft.com/mazhou/2017/12/12/c-7-series-part-7-ref-returns/ 背景 有两种方法可以将一个值传 ...
随机推荐
- CCF_201612-2_火车购票
http://115.28.138.223/view.page?gpid=T46 水. #include<iostream> #include<cstring> #includ ...
- To be contine ,NW NMM backup sqlserver failed.
Last time, we talk about separate under one cluster backup into two diffetent AG backup. Does it wor ...
- python机器学习——正则化
我们在训练的时候经常会遇到这两种情况: 1.模型在训练集上误差很大. 2.模型在训练集上误差很小,表现不错,但是在测试集上的误差很大 我们先来分析一下这两个问题: 对于第一个问题,明显就是没有训练好, ...
- rabbitmq在kubernetes中持久化集群部署
背景 Javashop电商系统的消息总线使用的事rabbitmq,在订单创建.静态页生成.索引生成等等业务中大量采用异步消息系统,这个对于mq高可用的要求有两个重要的考量: 1.集群化 2.可扩容 3 ...
- Golang robfig/cron 实现解析
robfig/cron是GO语言中一个定时执行注册任务的package, 最近我在工程中使用到了它,由于它的实现优雅且简单(主要是简单),所以将源码过了一遍,记录和分享在此. 文档:htt ...
- VFP控制Excel操作集
◆访问EXCEL:ExcelSheet = GetObject('','Excel.Sheet')返回结果为类,则成功.例:ExcelSheet = GetObject('','Excel.Sheet ...
- MongoDB oplog 详解
oplog 简介 oplog 是local库下的一个固定集合,Secondary就是通过查看Primary的oplog这个集合来进行复制的.每个节点都有oplog,记录从主节点复制过来的信息,这样每个 ...
- jq根据table的tr行数动态删除相应的行
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...
- [Python]获取字典所有值
方法一:Key Value 直接获取 databases = {1: 'Student', 2: 'School'} for k,v in databases.items(): print(k,v) ...
- Python学习(杂)
Python学习 两个for 循环同时输出+正则文章 zip(list1,list2) zip函数同时便利两个列表 import sys import requests import re from ...