[译]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/ 背景 有两种方法可以将一个值传 ...
随机推荐
- python 函数(实参与形参、传递参数)
函数 什么是函数?函数是带名字的代码块,用于完成具体的工作.写出一个函数后,就可以一直调用. 定义函数,函数的基本组成: 1.1 向函数传递参数 向函数中传递任意参数,这样打印出的结果就可以根据自己的 ...
- HDU_1455_dfs
http://acm.hdu.edu.cn/showproblem.php?pid=1455 int dfs(int all,int sum,int now),all代表剩余总长,sum,代表每段长, ...
- Luogu P1280 尼可的任务(dp)
题意: 时间为n,有k个任务,每个任务有一个开始时间和持续时间,从第一分钟开始,如果有开始的任务就要做,问最大空闲时间 n,k<=1e5 思路: 设 dp[i]为i~n时间中最大空闲时间,vec ...
- vuejs之vue和springboot后端进行通信
一.新建一个vue项目,建立好后的相关文件 查看一下新建好的vue项目的结构: 当前各个文件中的内容: App.vue:主入口 <template> <div id="ap ...
- Python3(十) 函数式编程: 匿名函数、高阶函数、装饰器
一.匿名函数 1.定义:定义函数的时候不需要定义函数名 2.具体例子: #普通函数 def add(x,y): return x + y #匿名函数 lambda x,y: x + y 调用匿名函数: ...
- NPOI word文档表格在新的文档中多次使用
最近有一个项目,涉及到文档操作,有一个固定的模版,模版中有文字和表格,表格会在新的文档中使用n多次 //获取模版中的表格FileStream stream = new FileStream(strPa ...
- vue中子组件触发父组件的方法
网上找了几种方法,下面这两种最实用,最明了 方法一:父组件方法返回是字符串或数组时用这种方法 子组件: <template> <button @click="submit& ...
- Debian 10 安装无线网卡驱动 (rtl8822be)
apt install firmware-realtek
- 图解Java设计模式之设计模式面试题
图解Java设计模式之设计模式面试题 1.1 Java设计模式内容介绍 1.1.1 先看几个经典的面试题 1.1.2 设计模式的重要性 1.1 Java设计模式内容介绍 1.1.1 先看几个经典的面试 ...
- MySQL存储过程和游标
一.存储过程 什么是存储过程,为什么要使用存储过程以及如何使用存储过程,并且介绍创建和使用存储过程的基本语法. 什么是存储过程: 存储过程可以说是一个记录集,它是由一些T-SQL语句组成的代码块,这些 ...