飞道的博客

VS2019: 调试版本下的2到3倍性能提升

232人阅读  评论(0)

好消息一则

我们最近在Visual Studio 的x86/x64 C++编译器中做了大量性能方面的改进,特别是针对VS默认的调试版本配置。
以Visual Studio 2019 v16.10 预览版2为例,我们测试发现,在调试版本下,性能提升可达2到3倍。这些改进主要是因为我们在运行时检查(/RTCs)中去掉了一些性能开销,这个编译器开关是默认就开启的。

默认的调试版本配置

当你在Visual Studio中以调试模式编译时,在默认情况下会有一些编译开关被传递到C++编译器中。今天的这篇文章主要会关注这三个编译开关:/RTC1, /JMC 和 /ZI。

请不要误会,这些编译开关对于调试来说还是非常有用的,但是加入它们之后会带来显著的性能开销,特别是当启用了/RTC1开关的时候。在这次的版本发布中,我们在尽可能地保持编译器查找Bug的能力并提供更加平滑的调试体验的同时,移除了那些不是那么必要的性能开销代码。

举个例子

考察以下代码:

 

下图是使用了/RTC1 /JMC和/ZI开关后编译器生成的x64汇编代码:

 

从上面的汇编代码可以看出,/JMC和/ZI开关添加了232个额外的字节到栈上(第5行)。这个栈空间占用并不是总是必要的。如果同时使用了/RTC1开关,它会尝试对分配的栈空间进行初始化(第10行),这会导致很多CPU周期的损耗。在这个特殊的例子中,尽管我们分配的栈空间对于/JMC和/ZI功能的运行是必要的,但是对它进行初始化却不是必要的。我们可以证明,在编译期,这些检查并不是必要的。在真实世界中的C++代码库中,还有很多类似这样的函数代码,这就是可以进行性能优化的地方。
所以,大概的意思,相信大家基本明白了,如果需要了解更多关于各个开关的深层次含义,请继续阅读。

/RTC1

这个开关等效于同时使用/RTCs和/RTCu这两个开关。/RTCs会使用0xCC来对函数的栈帧进行初始化,然后做各种运行时检查,检测未经初始化的本地变量,检测数组的溢出以及对栈指针进行有效性验证(仅x86平台)。
在上面的汇编代码第10行中,指令rep stosd是由/RTCs引入的,这条指令也显著地降低了性能。如果这个开关和/JMC或者/ZI一起使用,则情况会更糟。

和/JMC开关的交互

/JMC代表”我的代码调试”(Just My Code Debugging)功能,在调试过程中,它会自动跳过你未编写的功能(例如框架,库和其他非用户代码)。它通过在调用运行时库的入口中插入函数调用代码来进行工作。这有助于调试器区分用户代码和非用户代码。这里的问题是,将函数调用插入到项目中每个函数的入口中,意味着整个项目中不再有叶子函数。如果该功能最初不需要任何堆栈框架,那么现在将需要,因为根据Windows平台的AMD64 ABI,我们需要至少有四个用于功能参数的堆栈插槽(称为Param Home区域)。这意味着/RTC之前未初始化的所有函数,因为它们是叶子函数且没有堆栈框架,现在将被初始化。在程序中通常有很多叶子函数是正常的,尤其是当你使用大量的模板化代码库(例如C++ STL)时。在这种情况下,/MMC会很高兴吃掉你的某些CPU周期。这不适用于x86(32位),因为我们那里没有任何参数本机区域。

和/ZI开关的交互

我们将要讨论的下一个交互是与/ZI开关的。它使你的代码具有“编辑并继续”支持,这意味着您无需在调试过程中重新编译整个程序即可进行小的更改。

为了增加这种支持,我们向堆栈中添加了一些填充字节(填充字节的实际数量取决于函数的大小)。 这样,你可以将在调试会话期间添加的所有新变量分配在填充区域上,而无需更改总堆栈帧大小,并且你可以继续调试而不必重新编译代码。
你可能已经猜到了,更多的堆栈区域意味着更多的内容可以通过/RTC进行初始化,从而导致更多的开销。

解决方案

所有这些问题的根源是不必要的初始化。我们真的需要每次都进入初始化堆栈区域吗?不。当确实需要堆栈初始化时,可以在编译器中安全地证明。例如,当至少有一个地址获取变量,在函数中声明的数组或未初始化的变量时,需要使用它。对于其他所有情况,我们都可以安全地跳过初始化,因为无论如何我们都不会通过运行时检查找到任何有用的东西。

当你使用编辑并继续编译时,情况会变得更加复杂,因为现在你可以在调试会话中添加未初始化的变量,只有在初始化堆栈区域时才能检测到该变量。而且我们可能还没有做到这一点。为了解决此问题,我们在调试信息中包含了必要的位,并通过Debug Interface Access SDK对其进行了公开。此信息告诉调试器/ZI引入的填充区域在哪里开始和结束。它还告诉调试器该函数是否需要任何堆栈初始化。如果是这样,调试器将无条件地初始化此内存范围内的堆栈区域,以供你在调试会话期间编辑的功能使用。新变量始终分配在此初始化区域的顶部,并且我们的运行时检查现在可以检测您新添加的代码是否安全。

结果

我们在默认的调试配置中编译了以下项目,然后使用生成的可执行文件来运行测试。 我们注意到,我们尝试的所有项目的性能都提高了2到3倍。更多使用到了STL库的项目可能会看到更大的改进。

 

总结

虽然程序正式发布是以发行版本(Release)Release)为主,但是提升调试版本的性能,对于调试大型程序来说,也是不错的。

最后

Microsoft Visual C++团队的博客是我非常喜欢的博客之一,里面有很多关于Visual C++的知识和最新的开发进展。大浪淘沙,如果你对Visual C++这门古老的技术还是那么感兴趣,则可以经常去他们那(或者我这)逛逛。
本文来自:《2x-3x Performance Improvements for Debug Builds》


转载:https://blog.csdn.net/mmxida/article/details/116721666
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场