关于C# Span的一些实践

Span这个东西出来好久了,竟然由于5.0又火起来了。html

相关知识

在大多数状况下,C#开发时,咱们只使用托管内存。而实际上,C#为咱们提供了三种类型的内存:c#

  • 堆栈内存 - 最快速的内存,可以作到极快的分配和释放。堆栈内存使用时,须要用stackalloc进行分配。堆栈的一个特色是空间很是小(一般小于1 MB),适合CPU缓存。试图分配更多堆栈会报出StackOverflowException错误并终止进程;另外一个特色是生命周期很是短 - 方法结束时,堆栈会与方法的内存一块儿释放。stackalloc一般用于必须不分配任何托管内存的短操做。一个例子是在corefx中记录快速记录ETW事件:要求尽量快,而且须要不多的内存。
  • 非托管内存 - 经过Marshal.AllocHGlobalxMarshal.AllocCoTaskMem方法分配在非托管堆上的内存。这个内存对GC不可见,而且必须经过Marshal.FreeHGlobalMarshal.FreeCoTaskMem的显式调用来释放。使用非托管内存,最主要的目的是不给GC增长额外的压力,因此最常常的使用方式是在分配大量没有指针的值类型时使用。在Kestrel的代码中,不少地方用到了非托管内存。
  • 托管内存 - 大多数代码中最经常使用的内存,须要用new操做符来分配。之因此称为托管(managed),由于它是被GC(垃圾管理器)管理的,由GC决定什么时候释放内存,而不须要开发人员考虑。GC又将托管对象根据大小(85000字节)分为大对象和小对象。两个对象的分配方式、速度和位置都有不一样,小对象相对快点,大对象相对慢点。另外,两种对象的GC回收成本也不同。

    为防止非受权转发,这儿给出本文的原文连接:http://www.javashuo.com/article/p-pdvtbvdg-nw.html数组

问题的产生

问个问题:写了这么多年的C#,咱们有用过指针吗?有没有想过为何?缓存

咱们用个例子来回答这个问题:一个字符串,正常它是一个托管对象。安全

若是咱们想解析整个字符串,咱们会这么写:微信

int Parse(string managedMemory);

那么,若是咱们想只解析一部分字符串,该怎么写?app

int Parse(string managedMemory, int startIndex, int length);

如今,咱们转到非托管内存上:异步

unsafe int Parse(char* pointerToUnmanagedMemory, int length);
unsafe int Parse(char* pointerToUnmanagedMemory, int startIndex, int length);

再延伸一下,咱们写几个用于复制内存的功能:ide

void Copy<T>(T[] source, T[] destination); 
void Copy<T>(T[] source, int sourceStartIndex, T[] destination, int destinationStartIndex, int elementsCount);
unsafe void Copy<T>(void* source, void* destination, int elementsCount);
unsafe void Copy<T>(void* source, int sourceStartIndex, void* destination, int destinationStartIndex, int elementsCount);
unsafe void Copy<T>(void* source, int sourceLength, T[] destination);
unsafe void Copy<T>(void* source, int sourceStartIndex, T[] destination, int destinationStartIndex, 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''p''l''i''c''i''t' };
Span<char> fromArray = array;

此外,还有ReadOnlySpan,能够用来处理字符串或其余不可变类型:

ReadOnlySpan<char> fromString = "Hello world".AsSpan();

Span建立完成后,就跟普通的数组同样,有一个Length属性和一个容许读写的index,所以使用时就和通常的数组同样使用就好。

看看Span经常使用的一些定义、属性和方法:

Span(T[] array);
Span(T[] arrayint startIndex);
Span(T[] arrayint startIndex, int length);
unsafe Span(void* memory, int length);

int Length { get; }
ref T this[int index] { get; set; }

Span<T> Slice(int start);
Span<T> Slice(int start, int length);

void Clear();
void Fill(T value);

void CopyTo(Span<T> destination);
bool TryCopyTo(Span<T> destination);

咱们用Span来实现一下文章开头的复制内存的功能:

int Parse(ReadOnlySpan<char> anyMemory);
int Copy<T>(ReadOnlySpan<T> source, Span<T> destination);

看看,是否是很是简单?

并且,使用Span时,运行性能极佳。关于Span的性能,网上有不少评测,关注的兄弟能够本身去看。

Span的限制

Span支持全部类型的内存,因此,它也会有至关严格的限制。

在上面的例子中,使用的是堆栈内存。全部指向堆栈的指针都不能存储在托管堆上。由于方法结束时,堆栈会被释放,指针会变成无效值,若是再使用,就是内存溢出。

所以:Span实例也不能驻留在托管堆上,而只能驻留在堆栈上。这又引出一些限制。

  1. Span不能是非堆栈类型的字段

若是在类中设置Span字段,它将被存储在堆中。这是不容许的:

class Impossible
{

    Span<byte> field;
}

不过,从C# 7.2开始,在其余仅限堆栈的类型中有Span字段是能够的:

ref struct TwoSpans<T>
{

    public Span<T> first;
    public Span<T> second;

  1. Span不能有接口实现

接口实现意味着数据会被装箱。而装箱意味着存储在堆中。同时,为了防止装箱,Span必须不实现任何现有的接口,例如最容易想到的IEnumerable。也许某一天,C#会容许定义由结构体实现的结口?

  1. Span不能是异步方法的参数

异步在C#里绝对是个好东西。

不过对于Span,是另外一件事。异步方法会建立一个AsyncMethodBuilder构建器,构建器会建立一个异步状态机。异步状态机会将方法的参数放到堆上。因此,Span不能用做异步方法的参数。

  1. Span不能是泛型的代入参数

看下面的代码:

Span<byte> Allocate() => new Span<byte>(new byte[256]);

void CallAndPrint<T>(Func<T> valueProvider) 
{
    object value = valueProvider.Invoke();

    Console.WriteLine(value.ToString());
}

void Demo()
{
    Func<Span<byte>> spanProvider = Allocate;
    CallAndPrint<Span<byte>>(spanProvider);
}

一样也是装箱的缘由。

上面是Span的内容。

下面简单说一下另外一个常常跟Span一块儿提的内容:Memory

Memory

Memory是一个新的数据类型,它只能指向托管内存,因此不具备仅限堆栈的限制。

Memory能够从托管数组、字符串或IOwnedMemory中建立,传递给异步方法或存储在类的字段中。当须要Span时,就调用它的Span属性。它会根据须要建立Span。而后在当前范围内使用它。

看一下Memory的主要定义、属性和方法:

public readonly struct Memory<T>
{

    private readonly object _object;
    private readonly int _index;
    private readonly int _length;

    public Span<T> Span { get; }

    public Memory<T> Slice(int start)
    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, 0, buffer.Length)) > 0)
{
    ParseBlock(new ReadOnlyMemory<byte>(buffer, start: 0, length: bytesRead)); 
}

void ParseBlock(ReadOnlyMemory<byte> memory)
{
    ReadOnlySpan<byte> slice = memory.Span;
}

总结

Span存在很长时间了,只是5.0作了一些优化。

用好了,对代码是很好的补充和优化,用很差,就会有给本身刨不少个坑。

因此,耗子尾汁。

 


 

微信公众号:老王Plus

扫描二维码,关注我的公众号,能够第一时间获得最新的我的文章和内容推送

本文版权归做者全部,转载请保留此声明和原文连接

相关文章
相关标签/搜索