1. 程式人生 > >.Net Core技術研究-Span<T>和ValueTuple<T>

.Net Core技術研究-Span<T>和ValueTuple<T>

生成 種類 help 復雜 垃圾 method 兼容 對象 exce

性能是.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 開發人員更自然地使用在運行時可用的不同類型的內存,同時避免與它們相關的常見缺陷。這種統一帶來了性能提升的同時,也簡化了我們日常的編碼。

周國慶

2019/3/24

.Net Core技術研究-Span<T>和ValueTuple<T>