站点图标 江湖人士

.Net Core 使用Span提升性能直逼C++性能

.Net Core 使用Span<T>提升性能直逼C++性能

经过了将近4年的开发迭代,.Net的跨平台实现.Net Core终于从曾经的大饼成为了可以使用在生产环境上的成熟框架,虽然国内依旧鲜有公司愿意尝鲜,不过随着.Net环境的开放,攻击.Net封闭的声音也越来越少,这很显然是件好事。

随着.Net Framework API大部分迁移完毕,.Net开发组的目光开始朝向了另外一个被经常提及的问题——性能上。 在3月初 .Net Core 2.1 Preview 1的发布信息中,一个新东西吸引了我的目光——Span<T>。

什么是Span<T>?

在官方对Span<T>的说明中,它是一种新的值类型,用于表示一个连续的内存区域,它不会考虑这块内存是否与托管对象关联,直接在堆栈上与本机代码进行相互操作,并同时保证提供安全的访问方式。

C#最基本的值类型数组都可以被转换成对应的Span<T>类型。

var arr = new byte[10];
Span<byte> bytes = arr; // Implicit cast from T[] to Span<T>

官方对Span<T>进行了如下的定义:

public readonly ref struct Span<T>
{
  private readonly ref T _pointer;
  private readonly int _length;
  ...
}

可以看出,Span<T>中大量使用了C#的关键字——ref。

墙角的缝——ref(引用)

关键字ref,官方名称引用,很早就出现在了C#语言中。它是一个很有意思的关键字,我使用一个小示例来说明:当被声明为ref的变量进入一个方法体中后,一旦这个变量值发生变更,方法体外的原变量的值也会随之发生变化。

一旦某一个变量被申明为ref,传入方法中的就不再是一个具体的变量,而是该变量的引用

ref的出现使C#拥有了类似C/C++指针一样的功能,适当使用ref有助于提高C#程序的性能,不过对一个单个变量进行引用操作是远远不够的,到目前为止,C++对数组的操作速度依旧远远大于C#,C#在遍历数组并进行比对操作时往往会生成一个新的临时变量,小数据操作时往往看不出什么性能损耗,但量变引起质变,当有大量数据需要进行处理时,会造成了很大的性能差异。

而Span<T>的出现,就是为了解决C#遍历大型数据因创建临时对象所产生的性能损耗问题。

新的利器——Span<T>

现在,我们可以稍稍解读一下官方对Span<T>的定义:它是一种即将加入.Net核心库的新结构体,存储了一块连续内存区域中所有对象的引用,我们可以通过这些引用直接操作这块内存区域某个地址上的值而不会产生新的临时变量,下面的截图也验证了我们的解读是正确的。

我们可以通过Span与ref直接操作内存,整个过程没有产生具体的临时变量

接下来我直接使用博文.Net Core中使用ref和Span<T>提高程序性能中使用的测试用例来测试Span<T>的性能,将For循环次数提高到10000000次。

结果也令人感到欣喜,将近3倍的性能提升。

可以预见的未来

当 .Net Core 2.1正式发布时,所有涉及到IO操作以及内存操作的方法都会因使用Span<T>以及它的线程安全结构Memory<T>而获得巨大的性能提升,大规模基于Span<T>的重构将会到来,重构范围将涉及到通信协议解析,字符串处理,文件流操作,第三方编译器等多个方面,我们将拥有媲美c++的数据处理速度,一个更加高效的 .Net Core和一个更加完美的未来。


2018年3月19日更新

好吧我承认我之前的测试是有问题的,首先上面的截图是我在DeBug模式下截取的,然后用低复杂度的自用方法去与官方的int.parse去比较也是有问题的,那么接下来我修改代码并使用Release模式重新进行测试,以下是测试代码:

    public static class Spanxtension
    {
        public static int ParseToInt(this string code)
        {
            Int16 sign = 1;
            int num = 0;
            UInt16 index = 0;
            for (int idx = index; idx < code.Length; idx++)
            {
                char c = code[idx];
                num = (c - '0') + num * 10;
            }
            return num * sign;
        }


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

    class Program
    {
        static void Main(string[] args)
        {
            string content = "content-length:123";
            Stopwatch watch1 = new Stopwatch();
            List<int> result1 = new List<int>();
            watch1.Start();
            for (int j = 0; j < 10000000; j++)
            {
                result1.Add(content.Substring(15).ParseToInt());
            }
            watch1.Stop();
            Console.WriteLine("tTime Elapsed:t" + watch1.ElapsedMilliseconds.ToString("N0") + "ms");
            List<int> result2 = new List<int>();
            var code = content.ToCharArray();
            var span = code.AsSpan();
            watch1.Restart();
            for (int j = 0; j < 10000000; j++)
            {
                result2.Add(span.Slice(15).ParseToInt());
            }
            watch1.Stop();
            Console.WriteLine("tTime Elapsed:t" + watch1.ElapsedMilliseconds.ToString("N0") + "ms");
            Console.ReadLine();
        }
    }

现在,无论是string还是span都使用的相同的低复杂度算法来转换为int,并启用Release模式进行测试,进行多次测试并截取其中一张截图,结果如下:

与之前的结论并没有太大区别,只是多了个官方的int.parse的确很耗性能这个新结论。

退出移动版