.Net Core中使用ref和Span提升程序性能

1、前言

其实说到ref,不少同窗对它已经有所了解,ref是C# 7.0的一个语言特性,它为开发人员提供了返回本地变量引用和值引用的机制。
Span 也是创建在ref语法基础上的一个复杂的数据类型,在文章的后半部分,我会有一个例子说明如何使用它。 git

2、ref关键字

不管是ref仍是out关键,都是一种比较难以理解和操做的语言特性,如C语言中操做指针同样,这样的高级语法老是什么带来一些反作用,可是我不认为这有什么,并且不是每个C#开发者都要对这些内部运行的机制有着深入的理解,我以为不论什么复杂的东西只是为人们提供了一个自由的选择,风险和灵活性永远是不能兼容的。github

来看几个例子来讲明引用与指针的相同性,固然下面的使用方式早在C# 7.0以前就可使用了:算法

public static void IncrementByRef(ref int x)
{
    x++;
}

public unsafe static void IncrementByPointer(int* x)
{
   (*x)++;
}

上面两个函数分别是使用ref和非安全指针来完成参数+1。数组

int i = 30;
IncrementByRef(ref i);
// i = 31
unsafe{
   IncrementByPointer(&i);
}
// i = 32

下面是C# 7.0提供的特性:安全

1.ref locals (引用本地变量)

int i = 42;
ref var x = ref i;
x = x + 1;
// i = 43

这个例子中为本地 i 变量的引用 x, 当改变x的值时i变量的值也改变了。架构

2.ref returns (返回值引用)

ref returns是C# 7中一个强大的特性,下面代码是最能体现其特性的,该函数提供了,返回int数组中某一项的引用:函数

public static ref int GetArrayRef(int[] items, int index) => ref items[index];

经过下标取得数组中的项目的引用,改变引用值时,数组也会随之改变。性能

3、Span

System.Span 是.Net Core核心的一部分,在 System.Memory.dll 程序集下。目前该特性是独立的,未来可能会集成到CoreFx中; 学习

如何使用呢?在.Net Core 2.0 SDK建立的项目下引用以下NuGet包:this

<ItemGroup>
    <PackageReference Include="System.Memory" Version="4.4.0-preview1-25305-02" />
    <PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="4.4.0-preview1-25305-02" />
  </ItemGroup>

在上面咱们看到了使用ref关键字能够提供的相似指针(T*)的操做单一值对象方式。基本上在.NET体系下操做指针都不认为是一件好的事件,固然.NET为咱们提供了安全操做单值引用的ref。可是单值只是用户使用“指针”的一小部分需求;对于指针来讲,更常见的状况是操做一系列连续的内存空间中的“元素”时。

Span 表示为一个已知长度和类型的连续内存块。许多方面讲它很是相似T[]或ArraySegment ,它提供安全的访问内存区域指针的能力。其实我理解它更将是.NET中操做(void*)指针的抽象,熟悉C/C++开发者应该更明白这意味着什么。

Span 的特色以下:

  • 抽象了全部连续内存空间的类型系统,包括:数组、非托管指针、堆栈指针、fixed或pinned过的托管数据,以及值内部区域的引用
  • 支持CLR标准对象类型和值类型
  • 支持泛型
  • 支持GC,而不像指针须要本身来管理释放

下面来看下Span 的定义,它与ref有着语法和语义上的联系:

public struct Span<T> {
    ref T _reference;
    int _length;
    public ref T this[int index] { get {...} }
    ...
}

public struct ReadOnlySpan<T> {
    ref T _reference;
    int _length;
    public T this[int index] { get {...} }
    ...
}

接下来我会用一个直观的例子来讲明Span 的使用场景;咱们以字符截取和字符转换(转换为整型)为例:

若有一个字符串string content = "content-length:123",要转换将123转换为整型,一般的作法是先Substring将与数字字符无关的字符串进行截断,转换代码以下:

string content = "content-length:123";
Stopwatch watch1 = new Stopwatch();
watch1.Start();
for (int j = 0; j < 100000; j++)
{
    int.Parse(content.Substring(15));
}
watch1.Stop();
Console.WriteLine("\tTime Elapsed:\t" + watch1.ElapsedMilliseconds.ToString("N0") + "ms");

为何使用这个例子呢,这是一个典型的substring的使用场景,每次操做string都会生成新的string对象,固然不光是Substring,在进行int.Parse时重复操做string对象,若是大量操做就会给GC形成压力。

使用Span实现这个算法:

string content = "content-length:123";
ReadOnlySpan<char> span = content.ToCharArray();    
span.Slice(15).ParseToInt();
watch.Start();
for (int j = 0; j < 100000; j++)
{
    int icb = span.Slice(15).ParseToInt();
}
watch.Stop();
Console.WriteLine("\tTime Elapsed:\t" + watch.ElapsedMilliseconds.ToString("N0") + "ms");

这里将string转换为int的算法利用ReadonlySpan实现,这也是Span 的典型使用场景,官方给的场景也是如些,Span 适用于屡次复用操做连续内存的场景。

转换代码以下:

public static class ReadonlySpanxtension
{
    public static int ParseToInt(this ReadOnlySpan<char> rspan)
    {
        Int16 sign = 1;
        int num = 0;
        UInt16 index = 0;
        if (rspan[0].Equals('-')){
            sign = -1; index = 1;
        }
        for (int idx = index; idx < rspan.Length; idx++){
            char c = rspan[idx];
            num = (c - '0') + num * 10;
        }
        return num * sign;
    }

}

4、最后

上述两段代码100000次调用的时间以下:

String Substring Convert:
        Time Elapsed:   18ms
ReadOnlySpan Convert:
        Time Elapsed:   4ms

目前Span 的相关支持还够,它只是最基础架构,以后CoreFx会对不少API使用Span 进行重构和实现。可见.Net Core的性能往后会愈来愈强大。

相关Demo会上传到QQ群中。

  GitHub:https://github.com/maxzhang1985/YOYOFx 若是觉还能够请Star下, 欢迎一块儿交流。

  .NET Core 开源学习群:214741894

相关文章
相关标签/搜索