zoukankan      html  css  js  c++  java
  • Span<T>和ValueTuple<T>性能是.Net Core非常关键的特性

    Span<T>和ValueTuple<T>

    性能是.Net Core一个非常关键的特性,今天我们重点研究一下ValueTuple<T>和Span<T>.

    一、方法的多个返回值的实现,看ValueTuple<T>

    日常开发中,假如我们一个方法有多个返回值,我们可能会用Out出参,或者使用一个自定义类/匿名类型,或者Tuple<T>. 

    • Out出参可以使用,但是在编写Async方法时不支持。
    • 自定义类/匿名类型,需要我们根据返回值的结构,自定义一个类型,带来性能开销,同时增加了编码工作量,同时需要考虑跨域序列化的问题。
    • .Net Framework 4.0后引入了Tuple<T>元组,但是Item1,Item2,...不够友好,方法调用方需要了解分别代表的含义。

    现在我们看看ValueTuple<T>的实现

    C# 7支持返回多个值的语言特性,我们写两个示例代码Tuple<T>和ValueTuple<T>,对比一下:

    复制代码
     1         /// <summary>
     2         /// Tuple
     3         /// </summary>
     4         /// <returns></returns>
     5         private Tuple<string, List<int>> GetValues()
     6         {
     7             return new Tuple<string, List<int>>("C7", new List<int> { 1, 2, 3 });
     8         }
     9 
    10         /// <summary>
    11         /// ValueTuple
    12         /// </summary>
    13         /// <returns></returns>
    14         private (string, List<int>) GetValuesN()
    15         {
    16             return ("C7", new List<int> { 1, 2, 3 });
    17         }
    复制代码

    Tuple的示例中,代码声明了一个Tuple元组,内存在托管堆上统一管理,GC垃圾回收在指定时机下回收。

    ValueTuple示例中,编译器生成的代码使用的是ValueTuple,其本身就是一个struct,在栈上创建,这使我们既可以访问这个返回值数据,同时确保在包含的数据结构上不需要做垃圾回收。

    我们通过IL Spy看下编译后的代码:

    上图可以看到:

    第一个方法GetValues,返回class [System.Runtime]System.Tuple`2<string, class [System.Collections]System.Collections.Generic.List`1<int32>>,一个类的实例 

    第二个方法GetValuesN,返回valuetype [System.Runtime]System.ValueTuple`2<string, class [System.Collections]System.Collections.Generic.List`1<int32>>,一个值类型的实例。

    类是在托管堆中分配的 (由 CLR 跟踪和管理,并受垃圾收集的管制,是可变的),而值类型分配在堆栈上 (速度快且较少的开销,是不可变的)。

    System.ValueTuple 本身并没有被 CLR 跟踪,它只是作为我们使用的嵌入值的一个简单容器。

    ValueTuple<T>作为C#7.0支持方法多返回值,的确在底层实现上考虑了性能表现(内存),同时编码上给我们带来了更愉快的语法特性!

    二、从字符串操作看Span<T>

    大多数.Net开发场景,只使用到了托管堆(由CLR统一管理),其实.Net 有三种类型的内存可以使用,不过要看具体的使用场景。

    • 栈内存:我们通常分配的值类型的内存空间,比如 int, double, bool,……它非常快 (通常在 CPU 的缓存中使用),但大小有限 (通常小于 1 MB)。当然,有些开发人员会使用 stackalloc 关键字添加自定义对象,但要知道它们是有危险性的,因为在任何时间都可能发生 StackOverflowException,使我们的整个应用程序崩溃。
    • 非托管内存:没有垃圾收集器的内存空间,必须自己使用像 Marshal.AllocHGlobal 和 Marshal.FreeHGlobal 之类的方法预订和释放内存。
    • 托管内存 / 托管堆:通过GC垃圾收集器释放已经不再使用的内存空间,使我们大多数人都过着无忧无虑的程序员生活,很少有内存问题。

    上述三种类型的内存,都有各自的优缺点,特点的使用场景。如果我们设计一个兼容支持上述三种类型的Lib,需要分别提供两种实现,一种是支持托管堆的,一种是支持栈和非托管内存的。比如说SubString。

    我们先看第一种支持托管推的SubString实现:

    复制代码
     1 string Substring(string source, int startIndex, int length)
     2 {
     3             var result = new char[length];
     4             for (var i = 0; i < length; i++)
     5             {
     6                 result[i] = source[startIndex + i];
     7             }
     8 
     9             return new string(result);
    10 }
    复制代码

    上述方法内部声明了新的string对象和字符数组,这无疑带来了内存和CPU消息,实现的并不差,但是也不理想。

    继续看第二种支持栈和非托管内存的,使用 char*(是的,一个指针!) 和字符串的长度,并返回类似的指向结果的指针。实现上就有点小复杂了。

    此时,我们看.Net Core新引入的System.Memory命名空间下的Span<T>. 首先它是一个值类型 (因此没有被垃圾收集器跟踪),它试图统一对任何底层内存类型的访问。看一下它的内部结构:

    复制代码
      // Constructor for internal use only.
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    internal Span(ref T ptr, int length)
    {
                Debug.Assert(length >= 0);
    
                _pointer = new ByReference<T>(ref ptr);
                _length = length;
    }
    
     public ref T this[Index index]
    {
                get
                {
                    // Evaluate the actual index first because it helps performance
                    int actualIndex = index.GetOffset(_length);
                    return ref this [actualIndex];
                }
    }
    复制代码

    不管我们是使用字符串、char[] 甚至是未管理的 char* 来创建一个 Span<T>, Span<T> 对象都提供了相同的函数,比如返回索引中的元素。可以把它看作是 T[],其中 T 可以是任何类型的内存。

    我们用Span<T>编写一个 Substring() 方法

    Span<char> SubString(Span<char> source, int startIndex, int length)
     {
           return source.Slice(startIndex, length);
     }

    上述方法不返回源数据的副本,而是返回引用源的子集的 Span<T>,对比第一种SubString实现:没有重复数据,没有复制和复制数据的开销。

    总结一下:

    .NetCore中通过引入诸如 System.ValueTuple and Span<T> 之类的类型,使. net 开发人员更自然地使用在运行时可用的不同类型的内存,同时避免与它们相关的常见缺陷。这种统一带来了性能提升的同时,也简化了我们日常的编码。

  • 相关阅读:
    ddos(分布式拒绝服务)攻击防御措施
    arp_announce和arp_ignore 详细解说
    TCP三次握手和四次挥手
    ARP请求详解
    LVS/DR模式原理剖析(FAQs)
    nfs配置 /etc/exports
    LVS集群之十种调度算法及负载均衡-理论
    SSH 故障排查思路
    shell脚本基础和编写规范
    计算机操作系统概述
  • 原文地址:https://www.cnblogs.com/Leo_wl/p/10713992.html
Copyright © 2011-2022 走看看