ReSharper 高效攻略 – 组件合成、即时编译、UI 线程
在前一篇ReSharper性能系列文章中,我们已经看到将它作为一个单独的过程来运行是很有意义的。
这带来很多好处:Visual Studio和ReSharper都在自己的进程中运行,有自己的线程池和内存空间。
但这本身并不是修复性能的最佳方案。这是个很广泛的问题,但还有其他可以改进的领域。
我们将在本文中研究如何改进Visual Studio和ReSharper的启动。
在本系列中:
-
ReSharper性能简介系列
-
将ReSharper从进程中取出
-
组件合成、即时编译、UI线程
-
ReSharper 2018.1和2018.1.1的性能改进
开始吧!
组件合成
Visual Studio实际上是Visual Studio Shell的组合,提供VSPackages中定义的通用函数和主机模块,以及一些提供C#编辑、web工具、Azure工具、WPF设计器、重构工具等等的VSPackage。
所有这些组件都必须在某些点上加载,并且经常依赖于其他组件。
Visual Studio使用托管扩展性框架(MEF)来确定加载顺序和指定组件的相互依赖性。
ReSharper就是其中的一个组件,但也有自己的组件模型。拥有我们自己的组件模型有几个原因:
-
我们有许多独立产品,比如:dotPeek、dotMemory和dotTrace,以及ReSharper命令行工具,它们都依赖和重用ReSharper的组件。
-
第三方可以通过在ReSharper组件模型上扩展来实现附加功能。
ReSharper的组件模型非常先进。
可以在主机启动时、加载解决方案时、需要时加载组件……使用扩展代码中的属性注册组件,然后在合适的时候加载。
当Visual Studio启动时,ReSharper大约加载8100个组件,而每个解决方案约加载2500个组件。
虽然所有这些特点都是强大的而且可以扩展,但也有一些缺点:
-
必须在某些时刻实例化组件。
既可能是在主机进程启动时(可能是Visual Studio或比如dotPeek这样的某个独立工具),也可能是在加载或卸载某个解决方案时。
-
必须将所有组件加载到一个块中,以确保能够满足相互依存关系。
Visual Studio和ReSharper加载和发现组件的方法不同。
Visual Studio缓存组件模型(这很好,但有时也需要温和地推动),而当文件发生任何变更时,ReSharper透明地使其组件缓存无效。
扫描组件的过程非常快,但当前的ReSharper结构中也有个缺点:所有组件都在主线程的一个事务中创建。
这是个问题,如果在主线程上扫描和初始化组件需要9秒,很有可能Visual Studio会显示黄色通知栏,说明ReSharper减慢了Visual Studio启动。
还有另一个问题:Visual Studio缓慢地加载自己的一些组件。
但ReSharper有时必须强制Visual Studio加载这些组件,因为ReSharper的组件可能依赖于它们。
对于很多这类情况,我们正在努力使ReSharper也使用缓慢加载,以进一步减少初始启动时间。
JetBrains正在提升主线程的要求,以便在其他线程上加载组件。这也使缓慢、按需加载成为可能,从而进一步减少启动时间。
即时编译– JIT编译
C#编译器通常不会直接编译为机器代码,而是编译为通用中间语言(CIL)。
随后启动.NET应用时,会即时编译(JIT)该CIL代码,然后由.NET运行时执行。
这种方法无需为不同CPU类型和平台分发不同的可执行文件。
它帮我们发布一个可以在各种Windows操作系统上甚至可以跨平台运行的ReSharper程序集,Rider已证明了这点(它也使用ReSharper引擎)。
JIT编译过程经过高度优化:.NET运行时只编译正在执行的CIL代码,而不是整个程序集。
现在,让我们回头看看刚才谈到的ReSharper的组件合成……启动时加载了大量程序集,因为我们必须确定在启动时是否必须进行初始化工作,大量代码是否必须经过JIT处理且编译。这一切都在主线程上运行的事实也意味着主线程受此JIT编译的影响。
那我们如何解决这个问题呢?
一种方法是跳过JIT,并通过将所有CIL代码转换为提前(AOT)机器代码,例如使用ngen.exe(本地映像生成器)来使用显式编译。
我们已经试过让ReSharper使用ngen.exe,但因为一些原因,这不切实际:
-
如果在JetBrains运行AOT编译,安装程序会变得很大,因为必须发布不同平台的程序集。
-
如果把AOT编译作为ReSharper的一部分安装,则安装过程将延长,并且需要更高权限,很多客户不接受这样做。
我们将采用在JIT和AOT编译之间的一种方法,而不是提前编译。
类似于使用ProfileOptimization类,我们将随ReSharper(和其他工具)发布运行时配置文件,而不是在主线程上执行JIT,以便.NET运行时更有效地执行JIT:
-
该配置文件描述了加载时访问的类和方法,而不必依赖后期绑定来发现必须编译哪些CIL代码。
-
由于有了这种描述,我们可以在多个后台线程上运行JIT编译,从而加快整个进程。
如果分析dotPeek的启动,使用许多来自ReSharper的组件的独立反编译器,我们可以看到大多数正常工作的JIT都发生在主线程上。
在4核/8线程的机器上,启动dotPeek大约需要12秒。
当使用JIT配置文件时,情况看起来不一样。主线程在启动期间仍然非常活跃(毕竟是主线程),但大量JIT工作转移到不同的线程中(以下截屏中的CLR工作线程)。
在同一4核/8线程机器上,现在启动只需大约7秒。
您可能已经注意到第二个快照的JIT比第一个快照的多(17031毫秒对比7740 毫秒)。
如果您得到这个结果,太好了!
第二个更快的版本确实在启动时执行了更多JIT编译。这是由于启动后,我们创建的JIT配置文件包含超过我们需要的类和方法。
这给JIT编译器带来更多工作,但全部应用加载更快,而且有这些额外的方法“可以使用”,对应用使用期间很有价值。
在ReSharper上使用这种技术加载组件效果不错,只需大约5秒,而不是目前平均的9秒。
正在进行中!
JetBrains正在改进即时(JIT)编译的性能,以及使其使用多个后台线程。
这会极大缩短启动时间。
UI线程中执行太多工作以前,我们研究Visual Studio的性能快照,并且确定发生了大量垃圾收集工作。然而我们忽略了一个有意思的东西。
看看主(UI)线程:
我们已经发现ReSharper中的组件合成以及即时(JIT)编译是导致Visual Studio发出黄色无响应通知的一个原因。
作为Visual Studio编程体验中积极的参与者,ReSharper的确会向主/UI线程发出消息,例如:在编辑器中增加代码分析警告符号,或在UI中渲染其他元素。
结果是如果ReSharper调用任何无响应的Visual Studio函数,即使ReSharper并非真正的罪魁祸首,大家还是认为是它导致了无响应通知–。
在文档中说明了这种行为:
因此,虽然无响应通知是一种表征,但它不一定100%准确。
我们希望尽量收集信息来改进ReSharper,如果您遇到Visual Studio和ReSharper的性能问题,请收集性能快照并发送至JetBrains!并且 让微软也知道 。
JetBrains正在努力确保ReSharper不会长时间中断UI消息泵,这样将会减少错误的通知。
结合减少在UI线程上执行的工作,这样有助于改进整体性能!
结论
迄今为止,在这个关于ReSharper性能的系列文章中,我们介绍了JetBrains在这方面进行的许多高级改进。遗憾的是,没有解决这些问题的良方。
结合多方面的工作会更有效果,这也是为什么:
-
我们正在研究将ReSharper作为Visual Studio的一个子进程而不是主进程的一部分。
-
我们正在努力尝试在后台线程加载组件。
-
我们正在研究按需创建组件,进一步减少启动时间的可能性。
-
我们正在改进即时(JIT)编译的性能,以及使其使用多个后台线程。
-
我们正在确保ReSharper不会长时间中断UI线程和消息泵,从而减少错误的无响应通知。
所有这些改进本质上都是关于架构的,并将在下一版ReSharper和Rider中开始呈现。
只有一类性能问题我们尚未研究:随着时间推移而逐渐积累的更小的、本地的性能问题。
常常出现只有在不同环境下使用软件才会显现的问题。在下一篇并且也是本系列的最后一篇文章中,我们将讨论最新版ReSharper 2018.1和Rider代码库中进行的许多更小的性能改进。
留意我们的下一篇博文!
如果您遇到性能问题,切记查看Visual Studio性能指南 (ReSharper 2017.3+),还有我们的知识库文章加快ReSharper(和Visual Studio)。如果您遇到可以重现的具体的性能问题,而且您愿意收集并发送性能问题快照给我们,我们将深表感激。
博客文章组件合成、即时编译、UI线程–ReSharper性能简介系列将在.NET工具博客上首发。