在 Microsoft .NET Framework 团队中,我们始终认为对开发者而言改进性能至少与添加新的运行时功能和库 API 具有同等价值。 .NET Framework 4.5 在性能方面的投入可观,所有应用程序方案将因此受益。 此外,因为 .NET 4.5 是 .NET 4 的更新,所以即使是 .NET 4 应用程序也可从许多对现有 .NET 4 功能的性能改进中获益。
谈及使开发者能够提供令人满意的应用程序体验时,启动时间(请参阅 msdn.microsoft.com/magazine/cc337892)、内存使用量(请参阅 msdn.microsoft.com/magazine/dd882521)、吞吐量和响应能力确实很重要。 我们设定的目标是为不同的应用程序方案改进这些度量标准,然后设计更改以达到或超过这些标准。 在本文中,我将简要概述我们在 .NET Framework 4.5 中所做的一些主要性能改进。
CLR
在此版本中,我们重点关注: 利用多个处理器内核来改进性能,降低垃圾回收器的延迟以及提高本机映像的代码质量。 以下是一些主要的性能改进功能。
多核实时 (JIT):我们持续监视低级硬件改进并与芯片供应商合作以实现最佳的硬件辅助性能。 特别是,自从多核芯片推出后,我们在自己的性能实验室中使用了多核芯片,并且我们已做出适当的更改以利用这一特别的硬件更改;但是,最初这些更改只会为极少数客户带来益处。
目前,几乎每台 PC 至少都有两个内核,这就使得需要多个内核的新功能迅速得到了广泛应用。 在 .NET 4.5 开发初期,我们着手研究能否使用多个处理器内核共享 JIT 编译任务(特别是作为应用程序启动的一部分),从而优化总体体验。 作为该调查的一部分,我们发现数量足够多的托管应用程序具有阈值数最小的 JIT 编译方法,这使投资变得值得。
此功能运行时借助的是可能在后台线程上执行的 JIT 编译方法,而在多核计算机上,这些方法将在另一个内核上并行运行。 在理想情况下,第二个内核会快速超过应用程序的主线执行,因此对多数方法来说都是在需要时对其进行 JIT 编译。 为了解要编译哪些方法,此功能会生成跟踪所执行方法的配置文件数据,然后在稍后的运行中将通过此配置文件数据获得指导。 生成配置文件数据这一要求是您与此功能进行交互的主要方式。
在尽可能少增加代码的情况下,您可以使用运行时的这一功能显著缩短客户端应用程序和网站的启动时间。 特别是,您需要对 System.Runtime 命名空间中 ProfileOptimization 类的两种静态方法进行直接调用。 有关详细信息,请参阅 MSDN 文档。 请注意,默认情况下对 ASP.NET 4.5 应用程序和 Silverlight 5 应用程序启用此功能。
优化的本机映像:在某些版本中,我们已使您能够通过名为“生成本机映像”(NGen) 的工具将代码预编译到本机映像中。 由此得到的本机映像与使用 JIT 编译所获得的效果相比,通常会使应用程序的启动速度明显加快。 在此版本中,我们引入了名为“配置文件导引优化”(MPGO) 的辅助工具以优化本机映像布局,从而进一步提高性能。 MPGO 使用配置文件导引优化技术,与之前介绍的多核 JIT 概念类似。 应用程序的配置文件数据包括一个具有代表性的方案或者一组方案,可用于重新排列本机映像的布局,以便将启动时所需的方法和其他数据结构集中在一部分本机映像中,从而缩短启动时间并减少工作集(应用程序的内存使用量)。 在我们自己的测试和体验中,我们通常会看到 MPGO 为较大托管应用程序(例如,大型交互式 GUI 应用程序)带来的益处,并且我们强烈建议按这些方式使用此工具。
MPGO 工具可为中间语言 (IL) DLL 生成配置文件数据并将该配置文件作为资源添加到 IL DLL 中。 NGen 工具用于在分析之后预编译 IL DLL,并可根据配置文件数据的状态执行其他优化。 图 1 显示了相关流程。
图 1 使用 MPGO 工具的进程流
大型对象堆 (LOH) 分配器:许多 .NET 开发人员都曾请求获得针对 LOH 碎片问题的解决方案或者强制压缩 LOH 的方法。 要详细了解 LOH 的工作方式,可阅读 Maoni Stephens 在 2008 年 6 月发布的“CLR 全面透彻解析”专栏中发表的文章:msdn.microsoft.com/magazine/cc534993。 总而言之,任何大小为 85,000 字节或更大的对象都将在 LOH 上进行分配。 目前,不对 LOH 进行压缩。 压缩 LOH 将耗费大量时间,因为垃圾回收器将需要移动大型对象,而这是一个费用高昂的事情。 在回收 LOH 上的对象后,这些对象会在未被回收的对象之间留下一些可用空间,这将导致碎片问题。
更详细地解释,即,CLR 会列出没有被清除的对象的可用列表以供日后重用这些对象,从而满足大型对象的分配请求;相邻的被清除对象将组成一个自由对象。 如果活动大型对象之间的这些可用内存碎片的大小不足以在 LOH 中进一步进行对象分配,则程序最终会结束,并且因为无法选择压缩,我们很快会遇到问题。 这会导致应用程序变得无法响应,并最终导致内存不足异常。
在 .NET 4.5 中,我们进行了一些更改以便在 LOH 中有效使用内存碎片,这些更改主要针对我们管理可用列表的方式。 这些更改适用于工作站和服务器垃圾回收 (GC)。 请注意,这不会更改 LOH 对象 85,000 字节的限制。
服务器后台 GC:在 .NET 4 中,我们为工作站 GC 启用了后台 GC。 自此以后,我们发现更快频率的计算机的堆大小上限从几 GB 到几十 GB。 即使经过优化的并行回收器(例如,我们的回收器)也需要数秒钟才能回收如此大的堆,这妨碍了应用程序线程数秒钟。 服务器的后台 GC 为我们的服务器回收器提供了并行回收支持。 它可最大程度地减少长时间处于阻止状态的回收,同时继续保持高的应用程序吞吐量。
如果您使用的是服务器 GC,则无需执行任何操作即可利用这一新功能;服务器后台 GC 将自动打开。 客户端和服务器 GC 的高级后台 GC 的特征是相同的:
- 只有完整 GC(第 2 代)可在后台发生。
- 后台 GC 不进行压缩。
- 前台 GC(第 0 代/第 1 代 GC)可在进行后台 GC 期间发生。 服务器 GC 在专用服务器 GC 线程上完成。
- 完全阻止 GC 同样在专用服务器 GC 线程上发生。
异步编程
新的异步编程模型是作为 Visual Studio Async CTP 的一部分引入的,并且现在是 .NET 4.5 的一个重要部分。 您可以使用 .NET 4.5 中的这些新语言功能有效地编写异步代码。 C# 和 Visual Basic 中名为“async”和“await”的两个新语言关键字支持这一新模型。 .NET 4.5 还进行了更新以支持使用这些新关键字的异步应用程序。
MSDN 上的 Visual Studio 异步编程门户 (msdn.microsoft.com/vstudio/async) 是获取关于新语言功能和支持的示例、白皮书及讨论内容的绝佳资源。
并行计算库
已在 .NET 4.5 中对并行计算库 (PCL) 做出许多改进以增强现有 API 功能。
更快的轻型任务:System.Threading.Tasks.Task 和 Task<TResult> 类已经过优化,可在关键方案中使用更少的内存且更快地执行。 特别是,与创建任务和安排后续任务相关的案例的性能提高了多达 60%。
更多 PLINQ 查询并行执行:PLINQ 在它认为并行执行查询会带来更多损失(使操作变慢)时将回退为顺序执行。 这些决策是有根据的猜测且不总是完美无缺,在 .NET 4.5 中,PLINQ 将识别更多类型的可成功并行执行的查询。
更快的并发回收:对 System.Collections.Concurrent.ConcurrentDictionary<TKey, TValue> 做出了许多调整,以使其在特定方案中更快速地执行。
有关这些更改的更多详细信息,请访问并行计算平台团队的博客,网址为 blogs.msdn.com/b/pfxteam。
ADO.NET
空位压缩行支持:使用 SQL Server 2008 稀疏列功能的客户经常会遇到空数据。 使用稀疏列功能的客户可能会生成包含大量空列的结果集。 在此情况下,引入了行空位压缩(SQLNBCROW 标记或者仅 NBCROW)。 此功能通过将具有 NULL 值的多个列压缩为位掩码,缩小了从具有大量列的服务器发送的结果集行所使用的空间。 这将极大地帮助依据表格格式数据流 (TDS) 协议来对包含众多空列的数据进行压缩。
Entity Framework
自动编译 LINQ 查询:如今在您编写 LINQ to Entities 查询时,Entity Framework 会遍历 C#/Visual Basic 编译器生成的表达式树并将其转换(或编译)为 SQL,如图 2 所示。
Figure 2 转换为 SQL 的 LINQ to Entities 查询
但是,将表达式树编译为 SQL 会产生一些开销,尤其是对更复杂的查询而言。 在早期版本的 Entity Framework 中,如果您希望避免每次执行 LINQ 查询时必须为此性能损失承担相应开销,则必须使用 CompiledQuery 类。
此新版本的 Entity Framework 支持名为“自动编译 LINQ 查询”的新功能。 现在,您所执行的每个 LINQ to Entities 查询均已自动编译且位于 Entity Framework 查询计划缓存中。 每次额外运行查询时,Entity Framework 将在其查询缓存中查找该查询且不必再次完成整个编译过程。 可在 bit.ly/iCaM2b 中查阅更多相关内容。
Windows Communication Foundation 和 Windows Workflow Foundation
Windows Communication Foundation (WCF) 和 Windows Workflow Foundation (WF) 团队已完成此版本中的大量性能改进,例如:
- TCP 激活可扩展性改进: 客户报告了一个 TCP 激活问题,即,当有多个并发用户使用固定重新连接发送请求时,共享服务的 TCP 端口无法顺利扩展。 此问题在 .NET 4.5 中已得到解决。
- 对 WCF HTTP/TCP 的内置 GZip 压缩支持: 使用这一新型压缩功能,我们有望获得高达 5 倍的压缩比。
- 在内存使用量较高时,回收 WCF 宿主: 当内存使用量较高时(可配置旋钮),我们使用最近很少使用的 (LRU) 逻辑回收 WCF 服务。
- 对 WCF 的 HTTP 异步流支持: 我们在 .NET 4.5 中实现了此功能且获得了与异步流相同的吞吐量,但可伸缩性得到了增强。
- 对 WCF TCP 进行了第 0 代碎片改进。
- 针对大型对象优化了 WCF 的 BufferManager: 对于大型对象,已实现更好的缓冲池以避免产生更高的第 2 代 GC 成本。
- 使用表达式缓存改进了 WF 验证: 对于加载并执行 WF 的核心方案,我们有望获得高达 3 倍的改进。
- 已实现 WCF/WF 端到端 Windows 事件跟踪 (ETW): 虽然这并非性能改进功能,但它可帮助客户进行性能调查。
您可以在工作流团队博客(网址为 blogs.msdn.com/b/workflowteam)和 MSDN 库文章(网址为 bit.ly/n5VCtU)中找到更多详细信息。
ASP.NET
在共享托管已成为 .NET 4.5 的 ASP.NET 团队的两个主要性能目标的情形下,可改进站点密度(又定义为“每站点内存消耗”)和缩短站点的冷启动时间。
在共享托管方案中,许多站点共享同一计算机。 在这种环境中,流量通常很少。 一些托管公司提供的数据显示,多数情况下每秒的请求低于 1 rps,偶尔出现 2 rps 或更高的峰值。 这意味着许多工作进程在长时间(IIS 7 和更高版本中默认为 20 分钟)空闲后可能会关闭。 因此启动时间变得非常重要。 在 ASP.NET 中,这是网站接收并响应请求所需的时间,即工作进程关闭时与网站编译完成时。
我们在此版本中实现了一些功能以缩短共享托管方案的启动时间。 使用的功能包括:
- Bin 程序集集中(共享公共程序集): ASP.NET 卷影复制功能允许在不卸载 AppDomain 的情况下更新该应用程序域中使用的程序集(因为 CLR 会锁定正在使用的程序集,所以必须这样做)。 通过将应用程序程序集复制到单独位置(默认的 CLR 确定的位置或用户指定的位置)并从该位置加载这些程序集,可实现此目的。 这将允许在卷影副本处于锁定状态时更新原始程序集。 默认情况下,ASP.NET 会为 Bin 文件夹程序集打开此功能,以便能够在站点启动和运行时继续更新 DLL。
- 对于自定义 ASP.NET 控件、组件,或者需要在 ASP.NET 应用程序中引用以及跨站点中的各个页面共享的其他代码的已编译程序集 (DLL),ASP.NET 会将网站的 Bin 文件夹识别为一个特殊文件夹。 会在 Web 应用程序中的所有位置自动引用 Bin 文件夹中的已编译程序集。 ASP.NET 还会检测 Bin 文件夹中供网站使用的特定 DLL 的最新版本。 通常将旨在供 ASP.NET 站点使用的预先打包的应用程序安装到 Bin 文件夹,而不是全局程序集缓存中。
- ASP.NET 和 CLR 团队已发现,当多个站点驻留在同一服务器上且使用相同应用程序时,其中许多卷影副本 DLL 往往完全相同。 当从磁盘中读取这些文件并将其加载到内存中时,这将导致产生许多延长启动时间和增加内存消耗的冗余加载。 可通过对要遵循的 CLR 使用符号链接解决此问题,然后确定公共文件并在特殊位置(符号链接将指向的位置)对其进行测试。 ASP.NET 将自动为要运行的 Bin DLL 配置卷影复制。 共享托管方现在可以根据 ASP.NET 指南设置其计算机,以便最大限度地获得性能优势。
- 多核 JIT: 可参阅前面的“CLR”一节中的相关信息。 ASP.NET 团队使用多核 JIT 功能通过跨处理器内核扩展 JIT 编译来缩短启动时间。 此功能在 ASP.NET 中默认处于启用状态,因此您可以利用此功能,而无需执行任何其他操作。 您可以在 web.config 文件中使用以下设置来禁用此功能:
<configuration>
<!-- ...
-->
<system.web>
<compilation profileGuidedOptimizations="None" />
<!-- ...
-->
- 预取器: Windows 中的预取器技术在降低应用程序启动期间分页读取磁盘的成本方面非常有效。 现在,也在 Windows Server 上启用了预取器(但并非默认情况)。 要为高密度 Web 托管启用预取器,请在命令行中运行以下一组命令:
sc config sysmain start=auto
reg add "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management\PrefetchParameters" /v EnablePrefetcher /t REG_DWORD /d 2 /f
reg add "HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Prefetcher" /v MaxPrefetchFiles /t REG_DWORD /d 8192 /f
net start sysmain
- 您稍后可以更新 web.config 文件以在 ASP.NET 中使用它:
<configuration>
<!-- ...
-->
<system.web>
<compilation enablePrefetchOptimization
="true" />
<!-- ...
-->
- 为高密度 Web 托管优化 GC: GC 会影响站点的内存消耗,但可以对其进行调整以改善性能。 您可以优化或配置 GC 以提高 CPU 性能(降低回收频率)或降低内存消耗(即,更频繁的回收以尽快释放内存)。 若要启用 GC 优化,可在 Windows\Microsoft\v4.0.30319 文件夹的 aspnet.config 文件中选择 HighDensityWebHosting 设置,以便每个站点消耗更少的内存(工作集):
<configuration>
<!-- ...
-->
<runtime>
<performanceScenario
value="HighDensityWebHosting" />
<!-- ...
-->
可在 bit.ly/A66I7R 上的“ASP.NET 新版本入门”白皮书中找到有关 ASP.NET 性能改进的更多详细信息。
需要反馈
此处所列改进并不详尽。 还有更多用于改进性能的微小更改,忽略这些更改是为了仅在本文中介绍主要功能。 除此之外,.NET Framework 性能团队也始终致力于对 Windows 8 托管的 Metro 风格的应用程序进行性能改进。 在您下载并尝试 .NET Framework 4.5 和 Visual Studio 11 Beta for Windows 8 后,如有关于未来版本的任何反馈或建议,请将您的想法告诉我们。
术语表
共享托管: 又称为“共享 Web 托管”,高密度 Web 托管支持成百个(甚至上千个)网站在同一服务器上运行。 通过共享硬件成本,可以更低的成本维护每个站点。 此技术大大降低了网站所有者的进入门槛。
冷启动: 冷启动是启动尚未位于内存中的应用程序所需的时间。 您可以通过在系统重新启动之后启动应用程序来体验冷启动。 对于大型应用程序,冷启动可能需要几秒钟,因为内存中不存在所需页面(代码、静态数据、注册表等),而将这些页面加载到内存中需要进行费时的磁盘访问。
热启动: 热启动是启动已位于内存中的应用程序所需的时间。 例如,如果应用程序已在几秒前启动,则大多数页面可能已加载到内存中且操作系统将重新使用这些页面,这将节省较长的磁盘访问时间。 这就是应用程序在您第二次运行它时启动速度更快的原因(或者第二个 .NET 应用程序比第一个启动更快的原因,因为 .NET 的部分内容已加载到内存中)。
生成本机映像(或 NGen): 可参考在执行之前将中间语言 (IL) 可执行文件预编译为计算机代码的过程。 这会产生两个主要性能优势。 首先,通过避免在运行时编译代码,缩短了应用程序启动时间。 其次,通过允许跨多个过程共享代码页,改进了内存使用情况。 还有一个工具 NGen.exe,它可创建本机映像并将这些映像安装到本地计算机上的本机映像缓存 (NIC) 中。 当本机映像可用时,运行库会加载这些映像。
配置文件导引优化: 已证明配置文件导引优化可缩短本机和托管应用程序的启动和执行时间。 Windows 提供的工具集和基础结构可对本机程序集执行配置文件导引优化,而 CLR 提供的工具集和基础结构可对托管程序集执行配置文件导引优化,这称为“托管配置文件导引优化”(或 MPGO)。 Microsoft 中的许多团队使用这些技术来改进其应用程序的性能。 例如,CLR 对本机程序集(C++ 配置文件导引优化)和托管程序集(使用 MPGO)执行配置文件导引优化。
垃圾回收器: .NET 运行时支持自动内存管理。 它会跟踪托管程序进行的每次内存分配以及定期调用垃圾回收器,以查找不再使用的内存并将其重新用于新的分配。 垃圾回收器执行的重要优化是,它不会每次都搜索整个堆,而是将堆分为三代(第 0 代、第 1 代和第 2 代)。 有关垃圾回收器的详细信息,请阅读 2009 年 6 月发布的“CLR 全面透彻解析”专栏中的文章:msdn.microsoft.com/magazine/dd882521。
压缩: 在垃圾回收上下文中,当堆达到足够零散的状态时,垃圾回收器会通过移动活动对象以使其彼此靠近来压缩堆。 压缩堆的主要目的是使更大的内存块可用于分配更多对象。