.NET平台系列17 .NET5中的ARM64性能
.NET团队使.NET 5大大提高了常规性能和ARM64性能。在《.NET5中的性能改进》博客中可以查看总体改进情况。在这篇文章中,将描述我们专门针对ARM64进行的性能改进,并展示对我们使用的基准的积极影响。我还将分享一些我们已经确定并计划在将来的版本中进行性能改进的其他机会。
虽然我们在RyuJIT中对ARM64的支持已经工作了五年多,但我们所做的大部分工作是确保生成功能正确的ARM64代码。我们在评估为ARM64生成的代码RyuJIT的性能方面花费的时间很少。作为.NET5的一部分,我们的重点是在这个领域进行调查,找出RyuJIT中任何明显的问题,这些问题将提高ARM64代码质量(CQ)。由于Microsoft VC++团队已经支持Windows ARM64,因此我们与他们进行了协商,以了解他们在进行类似练习时遇到的CQ问题。
尽管解决CQ问题是至关重要的,但有时它的影响在应用程序中可能并不明显。因此,我们还希望对.NET库的性能进行明显的改进,以使针对ARM64的.NET应用程序受益。下面是我将用来描述我们在.NET 5上改进ARM64性能的工作的概要:
- .NET库中特定于ARM64的优化
- RyuJIT产生的代码质量评估和结果
在.NET Core 3.0中,我们引入了一项称为“硬件内在函数”的新功能,该功能可以访问现代硬件支持的各种矢量化和非矢量化指令。对于x86 / x64体系结构,.NET开发人员可以使用命名空间System.Runtime.Intrinsics和System.Runtime.Intrinsics.X86下的一组API访问这些指令。在.NET 5中,我们在System.Runtime.Intrinsics.Arm下为ARM32 / ARM64体系结构添加了大约384个API 。这涉及到实现这些API并使RyuJIT知道它们,以便它能够发出适当的ARM32/ARM64指令。我们还优化了Vector64和Vector128的方法,这些方法提供了创建和操作Vector64<T>和Vector128<T>数据类型的方法,大多数硬件内部API都在这些数据类型上运行。如果有兴趣,请参考示例代码用法以及此处的Vector64
和Vector128
方法的示例。您可以在此处查看“硬件固有”项目的进度。
在.NET Core 3.1中,我们使用x86 / x64内部函数优化了.NET库的许多关键方法。当在支持x86 / x64内部指令的硬件上运行时,这样做可以提高此类方法的性能。对于不支持x86 / x64内在函数的硬件(例如ARM机器),. NET将回退到这些方法的较慢实现。dotnet /运行时#33308列出此类.NET库方法。在.NET 5中,我们还使用ARM64硬件内在函数对这些方法中的大多数进行了优化。因此,如果您的代码使用任何这些.NET库方法,则它们现在将看到在ARM体系结构上运行的速度提高。我们将精力集中在已经使用x86 / x64内在函数进行了优化的方法上,因为这些方法是基于较早的性能分析(我们不想重复/重复)而选择的,并且我们希望该产品在各个平台上具有大致相似的行为。展望未来,当我们优化.NET库方法时,我们期望同时使用x86 / x64和ARM64硬件内在函数作为我们的默认方法。我们仍然必须决定这将如何影响我们接受的PR的政策。
对于在.NET 5中优化的每种方法,我将向您展示用于验证改进的低级基准方面的改进。这些基准与现实世界相去甚远。在后面的文章中,您将看到如何将所有这些有针对性的改进结合在一起,以在更大,更真实的场景中极大地改进ARM64上的.NET。
System.Collections
- System.Collections.BitArray
System.Numerics
- System.Numerics.BitOperations
System.SpanHelpers
System.Text
我们还在的几个类别中优化了方法。System.Text
- 中的方法在dotnet / runtime#38597和dotnet / runtime#39506中进行了优化。
System.Text.ASCIIUtility
System.Text.Unicode
在dotnet / runtime#38653,dotnet / runtime#39041和dotnet / runtime#39050中进行了优化System.Text.Encodings.Web
在dotnet / runtime中进行了优化#38707
在.NET中6,我们计划以优化其余的方法中所描述的dotnet /运行#41292,方法到地址的dotnet /运行#35033和合并工作,以优化用做本·亚当斯在DOTNET /运行#41097。System.Text.ASCIIUtility
System.Buffers
JsonReaderHelper.IndexOfLessThan
上面提到的所有度量均来自我们在8/6 / 2020、8 / 10/2020和8/28/2020的Ubuntu计算机上进行的性能实验室运行。
在典型情况下,应用程序在运行时使用JIT编译为机器代码。生成的目标机器代码非常有效,但缺点是必须在执行期间进行编译,这可能会在应用程序启动期间增加一些延迟。如果预先知道目标平台,则可以为该目标平台创建准备运行(R2R)本机映像。这就是所谓的提前编译(AOT)。它的优点是启动时间更快,因为在执行过程中不需要生成机器代码。目标机器代码已经以二进制形式存在,可以直接运行。AOT编译的代码有时可能不太理想,但最终会被最佳代码所取代。
在.NET 5之前,如果一个方法(.NET库方法或用户定义方法)调用了ARM64硬件内部API(System.Runtime.Intrinsics和System.Runtime.Intrinsics.Arm下的API),那么这些方法永远不会在AOT下编译,并且总是延迟到运行时进行编译。这对一些在启动代码中使用这些方法的.NET应用程序的启动时间产生了影响。在.NET5中,我们在dotnet/runtime#38060中解决了这个问题,现在能够对此类方法进行AOT编译。
ARM64中的内存屏障
通过一些基准测试,我们注意到 volatile 类的关键方法的热循环中易失性变量的访问。访问ARM64的易失性变量非常昂贵,因为它们引入了内存屏障指令。通过缓存volatile变量并将其存储在循环外部的局部变量(dotnet / runtime#34225,dotnet / runtime#36976和dotnet / runtime#37081)中,可以提高性能,如下所示。所有的测量单位都是纳秒。
System.Collections.Concurrent.ConcurrentDictionary
ARM内存模型
ARM体系结构具有弱有序的内存模型。处理器可以重新排序内存访问指令以提高性能。它可以重新排列指令,以减少处理器访问内存所需的时间。指令的写入顺序不受保证,而是可以根据给定指令的存储器访问成本来执行。这种方法不会影响单核计算机,但会对在多核计算机上运行的多线程程序产生负面影响。在这种情况下,会有指令告诉处理器不要在给定点重新安排内存访问。限制这种重新排列的这种指令的技术术语称为“内存屏障”。ARM64中的dmb指令充当了一个屏障,阻止处理器将指令移动到栅栏之外。您可以在ARM开发人员文档中阅读更多关于它的内容。
在代码中指定添加内存屏障的一种方法是使用volatile变量。使用volatile
,可以确保运行时,JIT和处理器不会重新安排对内存位置的读写,以提高性能。为此,dmb
每次对volatile
变量进行访问(读/写)时,RyuJIT都会为ARM64发出(数据存储屏障)指令。
- ARM64和大常量
在.NET5中,我们对处理用户代码中存在的大常量的方式进行了一些改进。我们开始消除dotnet / runtime# 39096中大常量的冗余负载,这为我们为所有.NET库生成的ARM64代码的大小提供了大约1%的精确度(准确地说是521K字节)。
值得注意的是,有时JIT的改进不会在微基准测试运行中得到体现,但会对整体代码质量有所帮助。在这种情况下,RyuJIT团队报告了.NET库代码大小方面的改进。RyuJIT在更改前后都在整个.NET库dll上运行,以了解优化产生了多大的影响,以及哪些库比其他库进行了更多的优化。从预览版8开始,用于ARM64目标的整个.NET库的发出代码大小为45 MB。1%的改进意味着.NET 5中我们减少了450 KB的代码,这是相当可观的。您可以在此处看到改进的方法的数量。
ARM64具有指令集体系结构(ISA),具有固定长度的编码,每条指令的长度恰好为32位。因此,移动指令mov
仅具有空间来编码最多16位无符号常量。要移动更大的常量值,我们需要使用16位块()逐步移动该值。因此,生成了多个指令以构造一个更大的常数,该常数需要保存在寄存器中。或者,在x64中,单个可以加载更大的常量。movz/movk
mov
mov。
数据驱动工程方法,用于发现其他重要的ARM64代码质量增强并对其进行优先级排序。当用几个基准检查为.NET库生成的ARM64代码时,我们意识到有几种指令模式可以用更好,性能更高的指令代替。在编译器文献中,“窥孔优化”是进行此类优化的阶段。RyuJIT当前没有窥视孔优化阶段。添加新的编译器阶段是一项艰巨的任务,并且很容易花费数月的时间才能使其正确完成,同时又不影响其他指标(如JIT吞吐量)。此外,我们不确定代码的大小或加快这种优化的速度能为我们带来多少。因此,我们以一种有趣的方式收集了数据,以发现窥视孔优化中的各种机会并确定其优先级。我们写了一个实用工具AnalyzeAsm它将扫描大约1GB的文件,其中包含.NET库方法的ARM64反汇编代码,并报告我们感兴趣的指令模式及其使用的方法的频率。有了这些信息,对于我们来说,确定窥视孔优化阶段的最小实施非常重要变得更加容易。使用AnalyzeAsm
,我们发现了一些窥孔,它们可以使.NET库的代码大小大致提高0.75%。在.NET 5中,我们通过消除dotnet / runtime#38179中的冗余相反mov
指令来优化指令模式,这为我们提供了0.28%代码大小的改善。从百分比的角度来看,改进并不大,但是对于整个产品而言,它们却是有意义的。
- 用【ldp】替换【ldr】对
- 用【stp】替换【str】对
- 用【str xzr】替换【str wzr】对
- 删除冗余的【ldr】和【str】
- 将【 ldr】替换为【mov】
- 使用movz / movk加载大常量
- 调用间接和虚拟存根
在Techempower基准测试中显着改善了ARM64性能。以下是针对请求/秒的度量(越高越好)
在.NET 5中,我们在提高ARM64目标的速度和代码大小方面取得了长足的进步。我们不仅在.NET API中公开了ARM64内在函数,而且还在我们的库代码中使用了它们以优化关键方法。通过我们的数据驱动工程方法,我们能够对.NET 5中具有高影响力的工作项目进行优先级排序。在进行性能调查时,我们还发现了dotnet / runtime#35853中总结的一些机会,我们计划继续为.NET工作。 6.我们与Arm Holdings的@TamarChristinaArm建立了良好的合作关系,他们不仅实现了一些ARM64硬件内在函数,而且还提供了宝贵的建议和反馈以提高我们的代码质量。我们要感谢多个贡献者,他们使得能够发布在ARM64目标上运行的.NET 5成为可能。
我们鼓励大家下载适用于ARM64的.NET 5最新版本,并让我们知道您的反馈。
参考文献:
- https://devblogs.microsoft.com/dotnet/Arm64-performance-in-net-5/
- 适用于ARMv8-A的ARM Cortex-A系列程序员指南 https://developer.arm.com/documentation/den0024/a/memory-ordering
.NET平台系列17 .NET5中的ARM64性能的更多相关文章
- .NET平台系列14 .NET5中的新增功能
系列目录 [已更新最新开发文章,点击查看详细] .NET5中不包含的内容 尽管 .NET5 框架中提供了一组重要 API,但它并不包括过去20年左右开发的所有 API,但是.NET Stand ...
- .NET平台系列15 .NET5的吊炸天性能改进
系列目录 [已更新最新开发文章,点击查看详细] .NET5的性能改进测试功能 Benchmark.NET现在是衡量.NET代码性能的规范工具,可轻松分析代码段的吞吐量和分配. .NET5的性能 ...
- .NET平台系列18 .NET5的超强优势
系列目录 [已更新最新开发文章,点击查看详细] 支持所有 .NET 应用程序类型 .NET5 统一版本之后将支持所有 .NET 应用程序类型:Xamarin.ASP.NET.IoT 和桌面.此 ...
- .NET平台系列16 .NET5/Asp.Net Core 在全球Web框架权威性能测试 Web Framework Benchmarks 中的吊炸天表现
系列目录 [已更新最新开发文章,点击查看详细] TechEmpower Web Framework Benchmarks 是许多Web应用程序框架执行基本任务(如JSON序列化.数据库访问和服 ...
- .NET平台系列13 .NET5 统一平台
系列目录 [已更新最新开发文章,点击查看详细] 时机决定一切,对于 .NET5 也是如此.实际上微软.NET团队在开始开发 .NET Core 时,对 .NET Framework 的全面重写 ...
- .NET平台系列21:云原生时代 .NET5 雄霸天下
系列目录 [已更新最新开发文章,点击查看详细] 随着互联网持续高歌猛进,相关技术名词也是层出不穷.微服务.容器化.DevOps.ServerLess.FaaS,这两年最火的当属云原生Cloud ...
- [转]asp.net5中使用NLog进行日志记录
本文转自:http://www.cnblogs.com/sguozeng/articles/4861303.html asp.net5中使用NLog进行日志记录 asp.net5中提供了性能强大的日志 ...
- asp.net5中使用NLog进行日志记录
asp.net5中提供了性能强大的日志框架,本身也提供了几种日志记录方法,比如记录到控制台或者事件中等,但是,对大部分程序员来说,更喜欢使用类似log4net或者Nlog这种日志记录方式,灵活而强大. ...
- .NET平台系列22:.NET Core/.NET5/.NET6 对比 .NET Framework
系列目录 [已更新最新开发文章,点击查看详细] 在我的博客<.NET平台系列2 .NET Framework 框架详解>与 <.NET平台系列7 .NET Core 体系结构 ...
随机推荐
- 1438. Longest Continuous Subarray With Absolute Diff Less Than or Equal to Limit
Given an array of integers nums and an integer limit, return the size of the longest continuous suba ...
- 【Java】 Java中的浅拷贝和深拷贝
先抛出结论: 浅拷贝是引用拷贝,A对象拷贝B以后,A对象和B对象指向同一块内存地址,改变A对象的属性值会触发B对象属性的改变,有安全风险 深拷贝是对象拷贝,A对象拷贝B以后,A对象和B对象指向不同的额 ...
- 技术面试问题汇总第004篇:猎豹移动反病毒工程师part4
这次所讨论的三个问题,比如DLL以及HOOK,很容易被病毒木马所利用,因此必须要比较全面地进行了解.而异常处理机制,则往往与漏洞相关联.它们自身的概念并不难理解,只是由之引申而来的问题,在计算机安全领 ...
- hdu1245 两个权值的最短路
题意: 求s到t的最短路,如果路径相同求那么要求另一个权值尽可能的小. 思路: 水题,就是spfa的比较那个地方多了一个可以更新的机会,当(s_x[xin] > s_x[ ...
- Java中常见的包
目录 JDK自带的包 第三方包 JDK自带的包 JAVA提供了强大的应用程序接口,既JAVA类库.他包含大量已经设计好的工具类,帮助程序员进行字符串处理.绘图.数学计算和网络应用等方面的工作.下面简单 ...
- 手动添加导入表修改EXE功能
目标: 改动PE导入表,手工给HelloWorld增加一个功能,就是启动的时候写入一条开机启动项,C:\cmd0000000000000000000000000000.exe 实现方法: 直接在注册相 ...
- Java发送邮件报错:com.sun.mail.util.LineOutputStream.<init>(Ljava/io/OutputStream;Z)V
在练习使用Java程序发送邮件的代码 运行出现了com.sun.mail.util.LineOutputStream.<init>(Ljava/io/OutputStream;Z)V报错信 ...
- Xshell6连Linux
一.安装 文件 链接: 提取码:8rmr 二.连Linux 名称填自己喜欢的.续之前,我们保持一样的名字.主机填IP,根据之前Linux填的静态IP去连接. 然后双击,连接 我们用最高权限,填root ...
- 异常检测算法Robust Random Cut Forest(RRCF)关键定理引理证明
摘要:RRCF是亚马逊发表的一篇异常检测算法,是对周志华孤立森林的改进.但是相比孤立森林,具有更为扎实的理论基础.文章的理论论证相对较为晦涩,且没给出详细的证明过程.本文不对该算法进行详尽的描述,仅对 ...
- Java GUI入门手册-AWT篇
Java GUI入门手册: AWT是基本的GUI设计工具,重点学习其中的布局格式以及事件监听事件. 首先创建一个窗口,我们先分析Frame类中的方法: 通过上图,可以看出frame是由构造方法的重载: ...