[译]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函数2(返回值、传递列表...)
python函数2(返回值.传递列表...) 1.返回值 1.1.返回简单的值 #返回简单值 def get_formatted_name(first_name,last_name): "& ...
- 第2章 Java并行程序基础(二)
2.3 volatile 与 Java 内存模型(JMM) volatile对于保证操作的原子性是由非常大的帮助的(可见性).但是需要注意的是,volatile并不能代替锁,它也无法保证一些复合操作的 ...
- Go语言实现:【剑指offer】二叉树中和为某一值的路径
该题目来源于牛客网<剑指offer>专题. 输入一颗二叉树的跟节点和一个整数,打印出二叉树中结点值的和为输入整数的所有路径.路径定义为从树的根结点开始往下一直到叶结点所经过的结点形成一条路 ...
- Xcode11: 删除默认Main.storyBoard, 自定义UIWindow的变化 UIWindow 不能在AppDelegate中处理
Xcode自动新增了一个SceneDelegate文件,查找了一下官方文档WWDC2019:Optimizing App Launch 发现,iOS13中appdelegate的职责发现了改变: iO ...
- Mysql 5.7 主从复制的多线程复制配置方式
数据库复制的主要性能问题就是数据延时 为了优化复制性能,Mysql 5.6 引入了 “多线程复制” 这个新功能 但 5.6 中的每个线程只能处理一个数据库,所以如果只有一个数据库,或者绝大多数写操作都 ...
- k8s系列---kubectl基础
kubectl get pods 查看所有pods kubectl get services 查看services kubectl replace --filename=myweb-rc.ya ...
- 珠峰-babel
#### babel 翻译的require为了给node使用么.浏览器可以使用么.#### amd, cmd的规范.和实现原理.#### babel的三个核心包,什么使用使用.#### babel的几 ...
- Python性能优化方案
Python性能优化方案 从编码方面入手,代码算法优化,如多重条件判断有限判断先决条件(可看 <改进python的91个建议>) 使用Cython (核心算法, 对性能要求较大的建议使用C ...
- Java的引用类型的内存分析
一. jdk的内存:jdk的bin目录常见命令 1. javac.exe:编译java源代码的,生成java字节码文件(*.class) 2. java.exe:启动一个jvm,来运行指定class字 ...
- jq模糊匹配(qq:2798641729)
图灵学院--Java高级架构师-互联网企业级实战VIP课程(价值6380)(qq:1324981084) jq是一般程序员在前台开发的时候都会使用的技术,其中模糊匹配查询在动态添加标签的时候经常用到, ...