无法识别的功能使程序速度降低5倍

放慢Windows第3部分:进程终止



作者致力于优化Google的Chrome的性能-大约。 反式

在2017年夏天,我为Windows性能问题苦苦挣扎。 进程终止速度很慢,已序列化并阻止了系统输入队列,这导致在组装Chrome时鼠标光标多次冻结。 主要原因是,在过程结束时,Windows花费了大量时间搜索GDI对象,而同时又限制了系统全局user32的关键部分。 我在文章“ 24核处理器,但我不能移动光标”中谈到了这一点。

Microsoft修复了该错误,然后我恢复了业务,但后来发现该错误又回来了。 有人抱怨LLVM测试运行缓慢,输入经常挂起。

但实际上,该错误并未返回。 原因是我们的代码更改。

2017年


每个Windows进程都包含几个标准的GDI对象描述符。 对于不处理图形的进程,这些描述符通常为NULL。 在此过程结束时,Windows将为这些描述符调用某些函数,即使它们为NULL。 没关系-功能运行很快-直到Windows 10 Anniversary Edition发行为止,在该版本中, 一些安全性更改使这些功能变慢了 。 在操作过程中,它们持有与输入事件相同的锁。 当大量进程同时终止时,每个进程都会对保持此关键锁的慢速函数进行多次调用,最终导致用户输入被阻止并且光标冻结。

微软的补丁是不为没有GDI对象的进程调用这些函数。 我不知道细节,但是我认为Microsoft补丁是这样的:

+ if (IsGUIProcess())
+ NtGdiCloseProcess();
– NtGdiCloseProcess();


也就是说,如果该进程不是GUI / GDI进程,则跳过GDI清理。

由于我们快速创建并终止的编译器和其他进程未使用GDI对象,因此该补丁足以修复UI冻结。

2018年


事实证明,实际上很容易为进程分配了一些标准GDI对象。 如果您的进程加载了gdi32.dll,则无论是否需要,您都将自动接收GDI对象(DC,曲面,区域,笔刷,字体等)(请注意,这些标准GDI对象不会显示在任务管理器中)在该流程的GDI对象中)。

但这不应该是一个问题。 我的意思是,为什么编译器会加载gdi32.dll? 好吧,事实证明,如果您加载user32.dll,shell32.dll,ole32.dll或许多其他DLL,那么您将自动获得另外的gdi32.dll(具有上述标准GDI对象)。 而且,意外下载其中一个库非常容易。

LLVM在加载每个名为CommandLineToArgvW (shell32.dll),有时还称为SHGetKnownFolderPath (也称为shell32.dll)的进程时进行测试。这些调用足以提取gdi32.dll并生成这些可怕的标准GDI对象。 由于LLVM测试套件生成了如此多的流程,因此最终会在流程完成时进行序列化,从而导致巨大的延迟和输入冻结,比2017年的情况严重得多。

但是这次我们知道阻塞的主要问题,因此我们立即知道该怎么做。

首先,我们摆脱了调用CommandLineToArgvW并 手动解析命令行的 麻烦 。 在那之后,LLVM测试套件很少从有问题的DLL中调用任何函数。 但是我们事先知道这不会以任何方式影响性能。 原因是,即使剩下的条件调用也足以始终提取shell32.dll,而后者又提取了gdi32.dll,后者创建了标准的GDI对象。

第二个解决方法是延迟加载shell32.dll 。 延迟加载意味着库将按需加载(在调用函数时),而不是在进程启动时加载。 这意味着shell32.dll和gdi32.dll将很少(并非总是)加载。

之后,LLVM测试套件开始运行五倍 ,而在一分钟而不是五分钟。 而且,开发机器上的鼠标不再冻结,因此员工可以在执行测试期间正常工作。 对于这样的适度更改而言,这是一个疯狂的加速,补丁的作者非常感谢我的调查,以至于他提名我获得公司奖金

有时,最小的变化会带来最大的后果。 您只需要知道在哪里拨打“零”

不接受执行路径


值得重复一遍的是,我们关注未执行的代码-这是一个关键的变化。 如果您有一个不访问gdi32.dll的命令行工具,则在加载gdi32.dll的情况下 ,使用条件函数调用添加代码会减慢该过程的速度。 在下面的示例中,从不调用CommandLineToArgvW ,但是即使代码中的简单存在(没有调用延迟)也会对性能产生负面影响:

 int main(int argc, char* argv[]) { if (argc < 0) { CommandLineToArgvW(nullptr, nullptr); // shell32.dll, pulls in gdi32.dll } } 

因此,是的,即使在某些情况下从未执行代码,删除函数调用也可能足以显着提高性能。

病理学复制


当我调查初始错误时,我编写了一个程序( ProcessCreateTests ),该程序创建了1000个进程,然后将它们并行杀死。 这重现了冻结,并且当Microsoft修复错误时,我使用了一个测试程序来检查补丁:请参见视频 。 修复该错误之后,我通过添加-user32选项更改了程序, 该选项为数千个测试进程中的每个进程加载user32.dll。 如预期的那样,使用该选项,所有测试过程的完成时间将大大增加,并且很容易检测到鼠标光标冻结。 使用-user32选项还会增加进程创建时间,但是在进程创建过程中没有游标暂停。 您可以使用该程序,看看问题可能有多严重。 这是我的四核/八线程笔记本电脑经过一周的正常运行时间后的一些典型结果。 -user32选项增加了所有操作的时间,但是进程上的UserCrit锁定特别明显地终止:

> ProcessCreatetests.exe
Process creation took 2.448 s (2.448 ms per process).
Lock blocked for 0.008 s total, maximum was 0.001 s.

Process destruction took 0.801 s (0.801 ms per process).
Lock blocked for 0.004 s total, maximum was 0.001 s.

> ProcessCreatetests.exe -user32
Testing with 1000 descendant processes with user32.dll loaded.
Process creation took 3.154 s (3.154 ms per process).
Lock blocked for 0.032 s total, maximum was 0.007 s.

Process destruction took 2.240 s (2.240 ms per process).
Lock blocked for 1.991 s total, maximum was 0.864 s.


挖掘更多只是为了好玩


我考虑了一些可用于更详细研究问题的ETW方法,并且已经开始编写它们。 但是我遇到了这种莫名其妙的行为,因此我决定另写一篇文章。 可以说,在这种情况下,Windows的行为更加奇怪。

该系列的其他文章:


文学作品


Source: https://habr.com/ru/post/zh-CN432076/


All Articles