性能是. 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>, 对比一下:
- /// <summary>
- /// Tuple
- /// </summary>
- /// <returns></returns>
- private Tuple<string, List<int>> GetValues()
- {
- return new Tuple<string, List<int>>("C7", new List<int> { 1, 2, 3 });
- }
- /// <summary>
- /// ValueTuple
- /// </summary>
- /// <returns></returns>
- private (string, List<int>) GetValuesN()
- {
- return ("C7", new List<int> { 1, 2, 3 });
- }
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 实现:
- string Substring(string source, int startIndex, int length)
- {
- var result = new char[length];
- for (var i = 0; i <length; i++)
- {
- result[i] = source[startIndex + i];
- }
- return new string(result);
- }
上述方法内部声明了新的 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
来源: https://www.cnblogs.com/tianqing/p/10589373.html