Span这个东西出来很久了,居然因为5.0又火起来了。
?
相关知识
在大多数情况下,C#开发时,我们只使用托管内存。而实际上,C#为我们提供了三种类型的内存:
- 堆栈内存 - 最快速的内存,能够做到极快的分配和释放。堆栈内存使用时,需要用
stackalloc 进行分配。堆栈的一个特点是空间非常小(通常小于1 MB),适合cpu缓存。试图分配更多堆栈会报出StackOverflowException 错误并终止进程;另一个特点是生命周期非常短 - 方法结束时,堆栈会与方法的内存一起释放。stackalloc 通常用于必须不分配任何托管内存的短操作。一个例子是在corefx中记录快速记录ETW事件:要求尽可能快,并且需要很少的内存。
- 非托管内存 - 通过
Marshal.AllocHGlobal 或xMarshal.AllocCoTaskMem 方法分配在非托管堆上的内存。这个内存对GC不可见,并且必须通过Marshal.FreeHGlobal 或Marshal.FreeCoTaskMem 的显式调用来释放。使用非托管内存,最主要的目的是不给GC增加额外的压力,所以最经常的使用方式是在分配大量没有指针的值类型时使用。在Kestrel 的代码中,很多地方用到了非托管内存。
- 托管内存 - 大多数代码中最常用的内存,需要用
new 操作符来分配。之所以称为托管(managed),因为它是被GC(垃圾管理器)管理的,由GC决定何时释放内存,而不需要开发人员考虑。GC又将托管对象根据大小(85000字节)分为大对象和小对象。两个对象的分配方式、速度和位置都有不同,小对象相对快点,大对象相对慢点。另外,两种对象的GC回收成本也不一样。
????为防止非授权转发,这儿给出本文的原文链接:https://www.cnblogs.com/tiger-wang/p/14029853.html
问题的产生
问个问题:写了这么多年的C#,我们有用过指针吗?有没有想过为什么?
我们用个例子来回答这个问题:一个字符串,正常它是一个托管对象。
如果我们想解析整个字符串,我们会这么写:
int?Parse(string?managedMemory);
那么,如果我们想只解析一部分字符串,该怎么写?
string?managedMemory,?int?startIndex,1); word-wrap: inherit !important; word-break: inherit !important">int?length);
现在,我们转到非托管内存上:
unsafe?(char*?pointerToUnmanagedMemory,1); word-wrap: inherit !important; word-break: inherit !important">int?length); unsafe?int?length);
再延伸一下,我们写几个用于复制内存的功能:
void?Copy<T>(T[]?source,?T[]?destination);? int?sourceStartIndex,?T[]?destination,1); word-wrap: inherit !important; word-break: inherit !important">int?destinationStartIndex,1); word-wrap: inherit !important; word-break: inherit !important">int?elementsCount); unsafe?void?Copy<T>(void*?source,1); word-wrap: inherit !important; word-break: inherit !important">void*?destination,1); word-wrap: inherit !important; word-break: inherit !important">int?sourceLength,?T[]?destination); unsafe?int?elementsCount);
是不是很复杂?而且看上去并不安全?
所以,问题并不在于我们能不能用,而在于这种支持会让代码变得复杂,而且并不安全 - 直到Span出现。
Span
在定义中,Span就是一个简单的值类型。它真正的价值,在于允许我们与任何类型的连续内存一起工作。
这些所谓的连续内存,包括:
在使用中,Span确保了内存和数据安全,而且几乎没有开销。
使用Span
要使用Span,需要设置开发语言为C# 7.2以上,并引用System.Memory 到项目。
<PropertyGroup> ??LangVersion>7.2</LangVersion> PropertyGroup>
使用低版本编译器,会报错:Error CS8107 Feature 'ref structs' is not available in C# 7.0. Please use language version 7.2 or greater. 。
?
Span使用时,最简单的,可以把它想象成一个数组,它会做所有的指针运算,同时,内部又可以指向任何类型的内存。
例如,我们可以为非托管内存创建Span:
Span<byte>?stackMemory?=?stackalloc?byte[256];
IntPtr?unmanagedHandle?=?Marshal.AllocHGlobal(256); Span<byte>?unmanaged?=?new?Span<byte>(unmanagedHandle.ToPointer(),?256);? Marshal.FreeHGlobal(unmanagedHandle);
从T[] 到Span的隐式转换:
char[]?array?=?new?char[]?{?'i',?'m',1); word-wrap: inherit !important; word-break: inherit !important">'p',1); word-wrap: inherit !important; word-break: inherit !important">'l',1); word-wrap: inherit !important; word-break: inherit !important">'c',1); word-wrap: inherit !important; word-break: inherit !important">'t'?}; Span<char>?fromArray?=?array;
?
此外,还有ReadOnlySpan,可以用来处理字符串或其他不可变类型:
ReadOnlySpan<char>?fromString?=?"Hello?world".AsSpan();
?
Span创建完成后,就跟普通的数组一样,有一个Length 属性和一个允许读写的index ,因此使用时就和一般的数组一样使用就好。
看看Span常用的一些定义、属性和方法:
Span(T[]?array); Span(T[]?array,1); word-wrap: inherit !important; word-break: inherit !important">int?startIndex); Span(T[]?int?length); unsafe?Spanvoid*?memory,1); word-wrap: inherit !important; word-break: inherit !important">int?length);
int?Length?{?get;?} ref?T?this[int?index]?{?get;?set;?}
Span<T>?Slice(int?start); Span<T>?Slice(int?start,1); word-wrap: inherit !important; word-break: inherit !important">int?length);
void?Clear(); Fill(T?value);
CopyTo(Span<T>?destination); bool?TryCopyTo(Span<T>?destination);
?
我们用Span来实现一下文章开头的复制内存的功能:
(ReadOnlySpan<char>?anyMemory); int?Copy<T>(ReadOnlySpan<T>?source,?Span<T>?destination);
看看,是不是非常简单?
而且,使用Span时,运行性能极佳。关于Span的性能,网上有很多评测,关注的兄弟可以自己去看。
Span的限制
Span支持所有类型的内存,所以,它也会有相当严格的限制。
在上面的例子中,使用的是堆栈内存。所有指向堆栈的指针都不能存储在托管堆上。因为方法结束时,堆栈会被释放,指针会变成无效值,如果再使用,就是内存溢出。
因此:Span实例也不能驻留在托管堆上,而只能驻留在堆栈上。这又引出一些限制。
- Span不能是非堆栈类型的字段
如果在类中设置Span字段,它将被存储在堆中。这是不允许的:
class?Impossible { ????Span<byte>?field; }
不过,从C# 7.2开始,在其他仅限堆栈的类型中有Span字段是可以的:
ref?struct?TwoSpans<T> { ????public?Span<T>?first; ????public?Span<T>?second; }?
- Span不能有接口实现
接口实现意味着数据会被装箱。而装箱意味着存储在堆中。同时,为了防止装箱,Span必须不实现任何现有的接口,例如最容易想到的IEnumerable 。也许某一天,C#会允许定义由结构体实现的结口?
- Span不能是异步方法的参数
异步在C#里绝对是个好东西。
不过对于Span,是另一件事。异步方法会创建一个AsyncMethodBuilder 构建器,构建器会创建一个异步状态机。异步状态机会将方法的参数放到堆上。所以,Span不能用作异步方法的参数。
- Span不能是泛型的代入参数
看下面的代码:
Span<byte>?Allocate()?=>?new?Span<byte>(new?byte[256]);
void?CallAndPrint<T>(Func<T>?valueProvider)? { ????object?value?=?valueProvider.Invoke();
????Console.WriteLine(value.ToString()); }
Demo() { ????Func<Span<byte>>?spanProvider?=?Allocate; ????CallAndPrint<Span<byte>>(spanProvider); }
同样也是装箱的原因。
?
上面是Span的内容。
下面简单说一下另一个经常跟Span一起提的内容:Memory
Memory
Memory是一个新的数据类型,它只能指向托管内存,所以不具有仅限堆栈的限制。
Memory可以从托管数组、字符串或IOwnedMemory中创建,传递给异步方法或存储在类的字段中。当需要Span时,就调用它的Span属性。它会根据需要创建Span。然后在当前范围内使用它。
看一下Memory的主要定义、属性和方法:
public?readonly?Memory<T> { ????private?readonly?object?_object; ????private?readonly?int?_index; ????int?_length;
????public?Span<T>?Span?{?get;?}
????public?Memory<T>?Slice(int?start) ????int?length) ????public?MemoryHandle?Pin() }
使用也很简单:
byte[]?buffer?=?ArrayPool<byte>.Shared.Rent(16000?*?8);
while?((bytesRead?=?await?fileStream.ReadAsync(buffer,1); word-wrap: inherit !important; word-break: inherit !important">0,?buffer.Length))?>?0) { ????ParseBlock(new?ReadOnlyMemory<byte>(buffer,?start:?ParseBlock(ReadOnlyMemory<byte>?memory) { ????ReadOnlySpan<byte>?slice?=?memory.Span; }
总结
Span存在很长时间了,只是5.0做了一些优化。
用好了,对代码是很好的补充和优化,用不好,就会有给自己刨很多个坑。
所以,耗子尾汁。
?
?
|