写点什么

性能是.NET Core 的一个关键特性

2018 年 5 月 23 日

关键要点

  • .NET Core 是跨平台的,可运行在 Windows、Linux、Mac OS X 和更多平台上;与.NET 相比,发布周期要短得多。大多数.NET Core 都是通过 NuGet 软件包交付的,可以很容易地发布和升级。

  • 更快速的发布周期对性能提升工作以及改进诸如 SortedSet 和 LINQ . tolist() 方法等语言结构性能的大量工作都有着特别的帮助。

  • 通过引入了 System.ValueTuple 和 Span 这样的类型,更快的周期和更容易的升级也为迭代改进 .NET Core 性能的新想法带来了机会。

  • 这些改进之后可以反馈到完整的 .NET 框架中。

随着.NET Core2.0 的发布,微软有了下一个主要版本的通用目标,模块化、跨平台和开源平台最初发布于 2016 年。.NET Core 已经创建了许多 api,这些 api 可以在.NET 框架的当前版本中使用。它最初是为下一代 ASP.NET 创建的解决方案,但现在是驱动、是许多其他场景的基础,包括物联网、云和下一代移动解决方案。在本系列中,我们将探讨一些.NET Core 的好处,以及它如何不仅能让传统的.NET 开发人员受益,还能让所有需要为市场带来健壮、高性能和经济解决方案的技术人员受益。

这篇InfoQ 文章是该系列文章".NET Core"的一部分。您可以订阅每周精要接收通知。

现在,.NET Core 正在路上,微软和开源社区可以在框架的新特性和增强上进行更快速的迭代。在.NET Core 中,性能是持续关注的一个领域: .NET Core 在执行速度和内存分配方面都带来了许多优化。

在这篇文章中,我们将讨论一些优化,以及如何在以后的性能工作中更多地使用连续流或Span,为我们的开发人员生活带来帮助。

.NET 和.NET Core

在深入研究之前,让我们先看看完整的.NET 框架(为方便起见我们称之为.NET) 和.NET Core 之间的主要区别。为了简化问题,让我们假设两个框架都遵循.NET 标准,它本质上是一个规范,定义了所有.NET 的基类库基线。这使得两个世界非常相似,除了两个主要的区别:

首先,.NET 主要是在Windows 上的,而.NET Core 是跨平台的,可运行在Windows、Linux、Mac OS X 和更多平台上。第二,发布周期非常不同。.NET 作为一个完整的框架安装程序,它是系统范围的,通常是Windows 安装的一部分,使发布周期更长。对于.NET Core,在一个系统上可以有多个.NET Core 安装,而且没有长时间的发布周期: 大多数.NET Core 是以NuGet 包交付的,可以轻松地发布和升级。

最大的优点是.NET Core 世界可以更快地迭代并文学地尝试新概念,并最终将它们反馈到完整的.NET 框架中,作为未来.NET 标准的一部分。

经常(但不总是),.NET Core 的新特性是由c#语言设计驱动的。因为框架可以更快地进化,语言也可以。一个快速发布周期和性能增强的主要例子是System.ValueTuple。c#和VB.NET 15 引入了“值元组”,这很容易添加到.NET Core,因为更快的发布周期,并且针对完整的.NET 4.5.2 和更早的版本,可以作为一个 NuGet 包用于完整的.NET ,在.NET 4.7 中也可以仅成为完整的.NET 框架的一部分。

现在让我们来看看其中的一些性能和内存改进。

.NET Core 的性能改进

.NET Core 工作的优点之一是,许多东西要么需要重新构建,要么需要从完整的.NET 框架中移植。让所有的内部构件在 flux 中运行一段时间,再加上快速发布周期,提供了一个在代码中进行一些性能改进的机会,以前,这些性能改进几乎被认为是“不要碰,它刚刚正常工作!“。

让我们从 SortedSet 和它的 Min 和 Max 的实现开始。SortedSet 是通过利用自平衡树结构,以有序顺序维护的对象集合。在此之前,从该集合中获取最小或最大对象需要向下遍历树 (或向上),调用每个元素的委托,并将返回值设置为当前元素的最小值或最大值,最终到达树的顶部或底部。调用该委托并传递对象意味着有相当多的开销。直到有一个开发人员看到了这棵树,并删除了不需要的委托调用,因为它没有提供任何值。他自己的基准测试显示有30%-50% 的性能提升。

另一个很好的例子是在LINQ 中,在常用的. tolist() 方法中更具体。大多数LINQ 方法在IEnumerable 上作为扩展方法操作,以提供查询、排序和诸如. tolist() 之类的方法。通过这样做,我们不必关心底层IEnumerable 的实现,只要 能够遍历它就行了。

缺点是,当调用. tolist() 时,我们不知道要创建的列表的大小,只枚举enumerable 中的所有对象,这把即将返回的列表的大小增加了一倍。这有点愚蠢,因为它潜在地浪费了内存(和CPU 周期)。因此,如果底层IEnumerable 实际上是具有已知大小的列表或数组,那么就会更改为创建一个已知大小的列表或数组。来自.NET 团队的基准测试显示,这些数据的吞吐量增加了4 倍。

当查看GitHub 上CoreFX 实验室存储库中的pull 请求时,我们可以看到微软和社区都做出了大量的性能改进。因为.NET Core 是开源的,你也可以提供性能修正。其中大多数都是:对.NET 中的现有类进行修复。但还有更多:.NET Core 还介绍了一些关于性能和内存的新概念,这些概念不仅仅是修复这些现有的类。让我们来看看本文其余部分的内容。

减少使用System.ValueTuple 的分配

假设我们想从一个方法返回多个值。以前,我们要么使用out 参数,这让人用起来非常不爽,而且在编写async 方法时也不支持。另一种选择是使用System.Tuple 作为返回类型,但它分配了一个对象,并且具有相当不友好的属性名称(Item1, Item2,…)。第三种选择是使用特定类型或匿名类型,但是在编写代码时这种做法会引入开销,因为我们需要定义类型,而且如果我们需要的是嵌入在该对象中的值,它也会造成不必要的内存分配。

遇到元组返回类型,由System.ValueTuple 支持。c# 7 和VB.NET 15 添加了一个语言特性,可以从一个方法返回多个值。下面是之前和之后的示例:

复制代码
// 之前:
private Tuple<string, int> GetNameAndAge()
{
return new Tuple<string, int>("Maarten", 33);
}
// 之后:
private (string, int) GetNameAndAge()
{
return ("Maarten", 33);
}

在第一个例子中,我们分配一个元组。因为几乎不必做什么额外的工作,分配是在托管堆上完成的,在某个时刻,垃圾回收(GC) 将不得不清理它。在第二种情况下,编译器生成的代码使用的是ValueTuple 类型,它本身就是一个struct,并在堆栈上创建,这使我们能够访问我们想要处理的两个值,同时确保在包含的数据结构上不需要做垃圾回收。

如果我们使用ReSharper 的中间语言(IL) 查看器查看编译器在上面示例中生成的代码,那么就会很明显看到这种差异。这里有两个方法签名:

复制代码
// 之前:
.method private hidebysig instance class [System.Runtime]System.Tuple`2<string, int32> GetNameAndAge() cil managed
{
// ...
}
// 之后:
.method private hidebysig instance valuetype [System.Runtime]System.ValueTuple`2<string, int32> GetNameAndAge() cil managed
{
// ...
}

我们可以清楚地看到第一个示例返回一个类的实例,第二个例子返回一个值类型的实例。类是在托管堆中分配的 (由 CLR 跟踪和管理,并受垃圾收集的管制,是可变的),而值类型分配在堆栈上 (速度快且较少的开销,是不可变的)。简而言之: System.ValueTuple 本身并没有被 CLR 跟踪,它只是作为我们关心的嵌入值的一个简单容器。

请注意,在其优化的内存使用情况下,像元组解构这样的特性是非常令人愉快的副产品,它使这部分语言和框架都成为了这一部分。

使用 Span减少子字符串的内存分配

在前一节中,我们已经讨论了栈和托管堆。大多数.NET 开发人员只使用托管堆,但.NET 有三种类型的内存可供使用,这取决于具体情况:

  • 栈内存——我们通常分配的值类型的内存空间,比如 int, double, bool,……它非常快 (通常在 CPU 的缓存中使用),但大小有限 (通常小于 1 MB)。富有挑战精神的开发人员会使用 stackalloc 关键字添加自定义对象,但要知道它们是有危险性的,因为在任何时间都可能发生 StackOverflowException,使我们的整个应用程序崩溃。

  • 非托管内存——没有垃圾收集器的内存空间,我们必须自己使用像 Marshal.AllocHGlobal 和 Marshal.FreeHGlobal 之类的方法预订和释放内存。

  • 托管内存 / 托管堆——垃圾收集器释放已经不再使用的内存空间,使我们大多数人都过着无忧无虑的程序员生活,很少有内存问题。

它们都有各自的优缺点,并有特定的用例。但是,如果我们想要编写一个与所有这些内存类型兼容的库该怎么办呢? 我们必须分别为他们提供方法。一个针对托管对象,另一个针对指针指向堆栈上或非托管堆上的对象。一个很好的例子就是创建一个字符串的子字符串。我们需要获取一个 System.String 并返回一个新 System.String 的方法,即要处理的托管版本的子字符串。非托管 / 堆栈版本将使用 char*(是的,一个指针!) 和字符串的长度,并返回类似的指向结果的指针。难以控制…

这个 System.Memory NuGet 包 (目前仍是预览版) 引入了一个新的 Span结构。它是一个值类型 (因此没有被垃圾收集器跟踪),它试图统一对任何底层内存类型的访问。它提供了一些方法,但本质上是这样的:

  • 一个 T 的引用

  • 一个可选的开始索引

  • 一个可选的长度

一些实用函数可以抓取一个 Span的切片,复制内容,…

把它想成这个 (伪代码):

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

不管我们是使用字符串、char[] 甚至是未管理的 char* 来创建一个 Span, Span对象都提供了相同的函数,比如返回索引中的元素。可以把它看作是 T[],其中 T 可以是任何类型的内存。如果我们想要编写一个子 Substring() 方法来处理所有类型的内存,那么我们所要关心的就是正在使用的 Span是如何工作的 (或者它的不可变版本,ReadOnlySpan):

ReadOnlySpan<char> Substring(ReadOnlySpan<char> source, int startIndex, int length);

这里的 source 参数可以是基于 System.String 的 span,或者是未管理的 char*,我们不需要关心。

而是让我们暂时忘掉内存类型不可知的方面,并关注性能。如果我们要为 System.String 编写一个 Substring() 方法,我们可能会想到的:

string Substring(string source, int startIndex, int length)

复制代码
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);
}

这很好,但实际上我们正在创建子字符串的副本。如果我们调用 Substring(“Hello World!”,0,5),我们在内存中有两个字符串: “Hello World”和“Hello”可能会浪费内存空间,我们的代码仍然需要将数据从一个数组复制到另一个数组,以实现这一点,消耗了 CPU 周期。我们的实现并不坏,但也不理想。

想象一下一个 web 框架的实现,它使用上面的代码从一个包含 header 和 body 的 HTTP 请求中获取请求体。我们必须分配具有重复数据的大块内存: 一个具有整个传入请求的内存和一个仅包含请求体的子字符串。然后是需要从原始字符串复制数据到子字符串的开销。

现在,让我们用 (ReadOnly)Span来重写它:

复制代码
static ReadOnlySpan<char> Substring(ReadOnlySpan<char> source, int startIndex, int length)
{
return source.Slice(startIndex, length);
}

好吧,它变短了,但其实还有更多变化。由于实现了方法 Span,所以我们的方法不返回源数据的副本,而是返回引用源的子集的 Span。或者在将 HTTP 请求拆分为 header 和 body 的例子中: 我们有 3 个 Span: 传入的 HTTP 请求,指向原始数据的头部分的一个 span,指向请求体的另一个 Span。数据在内存中只有一份 (创建第一个 Span 的数据),其他所有的数据只会指向原始数据的切片。没有重复数据,没有复制和复制数据的开销。

结论

随着.NET Core 和更快的发布周期,微软和.NET Core 的开源社区可以在与性能相关的新特性上快速迭代。我们已经看到框架中很多改进现有代码和构造的工作,比如改进 LINQ 的. tolist() 方法。

更快的周期和更容易的升级也带来了迭代改进.NET Core 性能的新想法的机会,通过引入诸如 System.ValueTuple and Span之类的类型,使. net 开发人员更自然地使用我们在运行时可用的不同类型的内存,同时避免与它们相关的常见缺陷。

想象一下,如果一些.net 基类被重写为 Span实现,诸如字符串 UTF 解析、加密操作、web 解析和其他典型的 CPU 和内存消耗任务。这将对框架带来很大的改进,并且所有的. net 开发人员都将受益。事实证明,这正是微软计划要做的事情! .NET Core 的性能前景光明!

关于作者

****Maarten Balliauw 喜欢构建 web 和云应用程序。他的主要兴趣是 ASP.NET MVC、 c#、Microsoft Azure、 PHP 和应用程序性能。他与别人共同创立了 MyGet,他还是 JetBrains 的开发人员。他是微软 Azure 的 ASP 内部人员和 MVP。Maarten 在不同的国家和国际活动中经常发言,并在比利时组织 Azure 用户组活动。在业余时间,他自己酿造啤酒。Maarten 的博客

随着.NET Core2.0 的发布,微软有了下一个主要版本的通用目标,模块化、跨平台和开源平台最初发布于2016 年。.NET Core 已经创建了许多api,这些api 可以在.NET 框架的当前版本中使用。它最初是为下一代ASP.NET 创建的解决方案,但现在是驱动、是许多其他场景的基础,包括物联网、云和下一代移动解决方案。在本系列中,我们将探讨一些.NET Core 的好处,以及它如何不仅能让传统的.NET 开发人员受益,还能让所有需要为市场带来健壮、高性能和经济解决方案的技术人员受益。

查看英文原文: Performance is a Key .NET Core Feature

2018 年 5 月 23 日 18:562521

评论

发布
暂无评论
发现更多内容

封装变化的内容

硬核编程

程序开发

图解 | 原来这就是 IO 多路复用

云流

Java 程序员 架构 面试

聊聊Java的异常机制问题

华为云开发者社区

Java 对象 异常机制 Throwable Error

程序开发必备的六个信条

硬核编程

程序开发

互联网大厂100道Android面试题助你冲关金三银四!附小技巧

欢喜学安卓

android 程序员 面试 移动开发

五年磨一剑,海外运营商数字化转型与新一代OSS

鲸品堂

方法论 数字化转型 运营商

力扣(LeetCode)刷题,简单题(第12期)

不脱发的程序猿

LeetCode 面试刷题 28天写作 算法面经 3月日更

从优秀到卓越:成为DevOps专家的7项软技能

禅道项目管理

DevOps 趋势 软技能

大侠请留步!欢迎有极客精神的你

Lily

Kubernetes弃用Docker运行时,小甜甜变牛夫人影响了谁?

会飞的鱼

Docker 云计算 架构 容器 #Kubernetes#

最火前端Web组态软件(可视化)

尔嵘

可视化数据分析搭建 前端可视化 web组态 托拉拽组态

AI量化智能交易软件,量化策略系统搭建

13823153121

颠覆认知——Redis会遇到的15个「坑」,你踩过几个?

Kaito

redis 踩坑 后端

中国云基础设施支出创新高,增速全球第一;国内首个区块链特色司法鉴定机构在京成立

京东科技开发者

区块链 人工智能 开发者

vue+element中引入百度地图

尔嵘

Vue Element 百度地图

使用VUE和Element 创建一个dialog对话框组件的详细过程

尔嵘

第九章学习总结

Kasn

产品经理 产品经理训练营

音频互动连麦使用手册

anyRTC开发者

ios android 音视频 WebRTC RTC

EGG Network公链生态应用EFTalk阿凡提

币圈那点事

产品经理训练营 - 作业六

胡小湖

layui使用templet格式化表格数据

通过序列号Sequence零代码实现订单流水号

crudapi

低代码 流水号 crud crudapi 序列号

第 9 周作业 _ 数据分析

园子

第九章作业

Kasn

产品经理 产品经理训练营

产品经理训练营作业 06

KingSwim

单片机如何从上电复位执行到main函数?

不脱发的程序猿

嵌入式软件 单片机 28天挑战 3月日更 上电复位执行到main函数

nginx做代理访问慢,优化方案

Ng

如何避免水肥一体化过量灌溉?开启智慧管理,一个屏幕轻松搞定

一只数据鲸鱼

物联网 数据可视化 智慧城市 智慧农业

世界首台人工智能地震监测系统问世;AAAI 2021 | 利用深度元学习对城市销量进行预测

京东科技开发者

大数据 红帽

智慧平安社区开发解决方案,智能安防小区综合管控平台建设

WX13823153201

nginx配置日志为json格式,nginx按照天实现日志分割,nginx配置负载均衡

Ng

演讲经验交流会|ArchSummit 上海站

演讲经验交流会|ArchSummit 上海站

性能是.NET Core的一个关键特性-InfoQ