读完上篇《通俗易懂,C#如何安全、高效地玩转任何种类的内存之Span的本质(一)。》,相信你们对span的本质应该很是清楚了。含着金钥匙出生的它,从小就被寄予厚望要成为.NET下编写高性能应用程序的重要积木,并且不少老前辈为了接纳它,都纷纷作出了改变,好比String、Int、Array。如今,它长大了,已经成为.NET下发挥关键做用的新值类型和旗舰成员。html
那咱们又该如何接纳它呢?git
一句话,熟悉它的脾气秉性,让好钢用到刀刃上。github
上篇博客介绍了span的本质,主要涉及到三个字段,以下:编程
public struct Span<T> { internal IntPtr _byteOffset; // 偏移量 internal object _reference;// 引用,能够看做当前对象的索引 internal int _length;// 长度 }
当咱们访问span表示的总体或部份内存时,内部的索引器经过计算(ref reference + byteOffset) + index * sizeOf(T)
来正确直接地返回实际储存位置的引用,而不是经过复制内存来返回相对位置的副本,从而达到高性能,可是,如今我要告诉你,这种span被叫作slow span,为何呢?由于C#7.2的新特性ref T
支持在签名中直接返回引用(至关于直接整合了这个过程),这样就无需经过计算来肯定指针开头及其起始偏移,从而真正拥有和访问数组同样高的效率,以下:c#
public struct Span<T> { internal ref T _reference;// 引用,自己已整合_byteOffset、_reference二者。 internal int _length;// 长度 }
这种只包含两个字段的span就叫Fast span。windows
在全部的.NET平台,Slow Span都是可获得的,可是目前只有.NET Core 2.X原生支持Fast span。数组
为了让你们更直观地了解这两种Span,下面来作两组基准测试安全
不一样运行时下Span进行10万次Get、Set的基准测试多线程
上图很是清楚了吧,从Mean(均值)指标能够看出差别仍是比较大的(约60%),net framework时代追求生产力,而core时代追求高性能,因此仍是早转core吧,而且新版本core还会进一步优化span,差距将会愈来愈大。并发
Span vs Array的基准测试
不一样运行时下,对Span和Array进行10万次Get、Set操做
从上图Mean(均值)指标能够得出:
看了上面测试,可能有的同窗就会问了用Array就好了,若是老是操做整个数组,这是合适的,但若是想操做数组的一部分数据呢?按照之前的作法每次复制一份相对位置的副本给调用方,这就很是消耗性能的,那么如何支持对完整或部分数组的操做保持一样高的性能呢?答案就是span,没有之一。span不只能用于访问数组和分离数组子集,还可引用来自内存任意区域的数据,好比本机代码、栈内存、托管内存。
分配一块栈内存是很是快速的,也无需手工释放,它会随着当前做用域而释放,好比方法执行结束时,就自动释放了,因此须要快取快用快放。Span虽然支持全部类型的内存,但决定安全、高效地操做各类内存的下限天然取决于最严苛的内存类型,即栈内存,比如木桶能装多少水,取决于最短的那块木板。此外,上一篇博客的动画很是清晰地演示了span的本质,每次都是经过整合内部指针为新的引用返回,而.NET运行时跟踪这些内部指针的成本很是高昂,因此将span约束为仅存在于栈上,从而隐式地限制了能够存在的内部指针数量。
备注:栈内存的容量很是小, ARM、x86 和 x64 计算机,默认堆栈大小为 1 MB。CLR和编译器会自动检测Stack-Only约束。
因此span必须是值类型,它不能被储存到堆上。
Span不能做为类的字段。
class Impossible { Span<byte> field; }
Span不能实现任何接口
先来看一段C#(伪代码):
struct StructType<T> : IEnumerable<T> { } class SpanStructTypeSample { static void Test() { var value = new StructType<int>(); Parse(value); } static void Parse(IEnumerable<int> collection) { } }
使用ILDasm查看生成的IL代码:
.method public hidebysig static void Test() cil managed // 调用Test方法 { // Code size 22 (0x16) .maxstack 1 .locals init (valuetype SpanTest.StructType`1<int32> V_0) IL_0000: nop IL_0001: ldloca.s V_0 IL_0003: initobj valuetype SpanTest.StructType`1<int32> IL_0009: ldloc.0 IL_000a: box valuetype SpanTest.StructType`1<int32> // 装箱,意味着被储存到托管堆上。 IL_000f: call void SpanTest.SpanStructTypeSample::Parse(class [System.Runtime]System.Collections.Generic.IEnumerable`1<int32>) IL_0014: nop IL_0015: ret } // end of method SpanStructTypeSample::Test
上面的代码很明确,首先让自定义的值类型实现接口IEnumerable,而后做为参数传递给Parse,最后分析IL代码发现参数被装箱了,意味着将被储存到托管堆上,若是未来C#能专门定义只用于struct的接口,那么就能扩展Stack-Only结构到此应用场景了,一块儿期待吧。
Span不能做为异步方法的参数
首先async
和await
是很是棒的语法糖,不只仅大大地简化了编写异步代码的难度,并且还带来了代码的优雅度。
一样,先来看一段C#代码:
public async Task TestAsync(Span<byte> data) { }
这样的用法也是禁止的,编译时就会报错Parameter or local type Span<byte> cannot be declared in async method.
。由于本质上,async
& await
的内部是经过AsyncMethodBuilder
来建立一个异步的状态机,某一时刻可能会将方法参数储存到托管堆上。
Span不能做为泛型类型的参数
一样,先来看一段C#代码:
Func<Span<byte>> valueProvider = () => new Span<byte>(new byte[256]); object value = valueProvider.Invoke(); // 装箱
这样的用法也是禁止的,编译时会报错The type Span<byte>may not be used as a type argument.
。同理,span<byte>
能够表示内存任意区域,而实际使用时确定须要类型化对象,没法避免装箱。那么微软为何不引入一种新的泛型约束:stackonly
,而是决定禁止span做为泛型参数,由于这须要编译器检查全部的代码,可能还须要理解代码逻辑(由于有的类型须要运行时才能肯定),否则是没法保证stackonly
约束的,呵呵,目前看来是不现实的,不知人工智能可否解决这个问题。
阐述这个特色前,先简单说说计算机的字大小。
计算机的字大小
表示计算机中CPU的字长,32位CPU字长为32位,即4字节;64位CPU字长为64位,即8字节。CPU的字长决定了每次可以原子更新的连续内存块的大小。
栈撕裂实际上是多线程下的数据同步问题,当结构数据大于当前处理器的字大小时,都会面临这个问题。如前所述,span内部包含多个字段,这就意味着,一些处理器可能没法保证原子更新span
的_reference
和_length
字段,也就是说,多线程下_reference
和_length
可能来自于两个不一样的span。
internal class Buffer { Span<byte> _memory = new byte[1024]; public void Resize(int newSize) { _memory = new byte[newSize]; // 由于这里没法保证原子更新 } public byte this[int index] => _memory[index]; // 因此这里可能的部分更新 }
其实有两种办法能够解决这个问题:
若是这样,就没法保证像数组同样的高性能,所以不能给字段加锁,也不能限制访问(没意义),另外对Span
的访问和写入都是直接操做的内存,若是_reference
和_length
出现不一样步的状况,还会致使内存安全问题。
这也是为何span只能存在于栈上,即指针、数据、长度全都存于栈上,而不是引用存在栈,数据存在堆,由于span<T>
不须要暂留,必须快取快用快放,不然就不要使用span。
备注:对于须要暂留到堆上的场景,它的解决方案是
Memory<T>
,你们能够继续关注。
为了支持轻松高效地处理 {ReadOnly}Span
下面是一些比较经常使用的扩展:
基元类型(伪代码)
short.Parse(ReadOnlySpan<char> s); int.Parse(ReadOnlySpan<char> s); long.Parse(ReadOnlySpan<char> s); DateTime.Parse(ReadOnlySpan<char> s); TimeSpan.Parse(ReadOnlySpan<char> input); Guid.Parse(ReadOnlySpan<char> input);
字符串
public static ReadOnlySpan<char> AsSpan(this string text, int start, int length); public static ReadOnlySpan<char> AsSpan(this string text, int start); public static ReadOnlySpan<char> AsSpan(this string text); public static String Create<TState>(int length, TState state, SpanAction<char, TState> action);
数组
public static Span<T> AsSpan<T>(this T[] array, int start); public static Span<T> AsSpan<T>(this T[] array); public static Span<T> AsSpan<T>(this ArraySegment<T> segment, int start, int length); public static Span<T> AsSpan<T>(this ArraySegment<T> segment, int start); public static Span<T> AsSpan<T>(this T[] array, int start, int length);
Guid
public static bool TryParse(ReadOnlySpan<char> input, out Guid result); public bool TryFormat(Span<char> destination, out int charsWritten, ReadOnlySpan<char> format = default (ReadOnlySpan<char>));
最后使用上面的API演示一个官网的例子,解析字符串"123,456"中的数字:
之前的写法:
var input = "123,456"; var commaPos = input.IndexOf(','); var first = int.Parse(input.Substring(0, commaPos));// yes-Allocating, yes-Coping var second = int.Parse(input.Substring(commaPos + 1));// yes-Allocating, yes-Coping
如今的写法:
var input = "123,456"; var inputSpan = input.AsSpan(); var commaPos = input.IndexOf(','); var first = int.Parse(inputSpan.Slice(0, commaPos));// no-Allocating, no-Coping var second = int.Parse(inputSpan.Slice(commaPos + 1));// no-Allocating, no-Coping
固然仍是有许多这样的方法,好比System.Random、System.Net.Socket、Utf8Formatter、Utf8Parser等,明白了它的脾气秉性,对于具体的应用场景你们能够先自行查阅资料,相信认真读完上篇、本篇的同窗已经具有用好这把尖刀的能力了。
综上所诉,经过限制Span只能驻留到栈上,完美解决了如下的问题:
备注:正是因为Stack-Only这个特色,在底层数据访问、转换以及同步处理方面,Span性能很是出色。
此外,本篇还在上篇的基础上,详细讲解span的脾气秉性,以及每种特色下的非法应用场景,一切都是为了你们可以在.NET 程序中使用span高效安全地访问内存,但愿你们能有所收获。下一篇可能会讲span的增强,也可能会讲它在数据转换以及同步处理方面的应用,好比:Data Pipelines、Discontinuous Buffers、Buffer Pooling等,也可能会讲Memory<T>
,感兴趣请继续关注。
若是有什么疑问和看法,欢迎评论区交流。
若是你以为本篇文章对您有帮助的话,感谢您的【推荐】。
若是你对高性能编程感兴趣的话能够关注我,我会按期的在博客分享个人学习心得。
欢迎转载,请在明显位置给出出处及连接。
https://github.com/dotnet/coreclr/blob/master/src/System.Private.CoreLib/shared/System/Span.Fast.cs
https://github.com/dotnet/coreclr/blob/master/src/System.Private.CoreLib/shared/System/Span.cs
https://docs.microsoft.com/en-us/dotnet/csharp/write-safe-efficient-code