ReSharper Platform Tutorials 开发工具

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就是其中的一个组件,但也有自己的组件模型。拥有我们自己的组件模型有几个原因:

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并非真正的罪魁祸首,大家还是认为是它导致了无响应通知–。

文档中说明了这种行为:

 

UI无响应或崩溃通知意味着当UI无响应或发生崩溃时,某个扩展的模块在堆栈中。

 

这并不一定意味着扩展本身就是罪魁祸首。

 

有可能是扩展调用的代码是Visual Studio的一部分,结果导致无响应UI或崩溃。

 

因此,虽然无响应通知是一种表征,但它不一定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工具博客上首发。

image description

Discover more