调试版本的性能提高了2-3倍

在VisualStudio默认调试配置中,我们在X86/X64 C++编译器中实现了大量的运行时性能改进。为了 Visual Studio 2019 版本16.10预览版2 ,我们测量 2倍- 3倍加速 编译的程序 在里面 调试 模式。 这些 改进 来自 减少 开销 由运行时检查(/RTCs)引入 哪些是 启用 通过 违约。

null

默认调试配置

在Visual Studio中以调试配置编译代码时 工作室,有一些旗子 通过 默认情况下,C++编译器。与此博客文章最相关 是 /实时时钟1 , /联合军委会 /子 .

而 全部 这些旗子 添加有用的调试功能,它们之间的交互,特别是当涉及/RTC1时, 添加 巨大的开销。 在 在这个版本中,我们消除了不必要的开销,同时确保 他们 不断帮助您发现bug,使调试体验更加顺畅。

考虑以下简单函数:

1    int foo() {
2        return 32;
3    }

使用/RTC1/JMC/ZI编译时由16.9编译器生成的x64程序集( 导柱连杆 ):

1    int foo(void) PROC                  
2    $LN3:
3            push rbp
4            push rdi
5            sub rsp, 232                ; extra space allocated due to /ZI, /JMC
6            lea rbp, QWORD PTR [rsp+32]
7            mov rdi, rsp
8            mov ecx, 58                 ; (= x)
9            mov eax, -858993460         ; 0xCCCCCCCC
10           rep stosd                   ; write 0xCC on stack for x DWORDs
11           lea rcx, OFFSET FLAT:__977E49D0_example@cpp
12           ; call due to /JMC
13           call __CheckForDebuggerJustMyCode
14           mov eax, 32
15           lea rsp, QWORD PTR [rbp+200]
16           pop rdi
17           pop rbp
18           ret 0
19    int foo(void) ENDP

在上面显示的程序集中,/JMC和/ZI标志在堆栈上总共添加了232个额外的字节(第5行)。这个堆栈空间并不总是必要的。当与/RTC1标志结合使用时,它会初始化分配的堆栈空间(第10行),它会消耗大量的CPU周期。在这个特定的示例中,尽管我们分配的堆栈空间对于/JMC和/ZI的正常运行是必需的,但它的初始化并不是必需的。我们可以在编译时证明这些检查是不必要的。在任何真实世界的C++代码库中都有很多这样的功能,这就是性能效益的来源。

继续阅读,深入了解每一面旗帜, 他们的互动 使用/RTC1, 以及我们如何避免 它的 不必要的开销。

/实时时钟1

使用 /实时时钟1 旗帜 相当于 两者兼用 /实时时钟 和 /RTCu公司 旗帜。 /实时时钟 通过0xCC初始化函数的堆栈帧 执行各种运行时检查 也就是说, 检测未初始化的局部变量, 检测 数组 溢出和欠载,以及堆栈指针验证 (对于x86)。 你可以看到代码膨胀 具有 /实时时钟 在这里 .

作为 如上图所示 装配 代码 (第10行) rep stosd 指令,由/RTCs介绍, 是经济放缓的主要原因。当/RTCs (或/RTC1) 已使用 结合 与/JMC合作, /或者两者都有。

与/JMC的互动

/联合军委会 代表 只是 我的 代码 调试 功能 , 在调试期间,它会自动跳过非您编写的函数(如框架、库和其他非用户代码)。它的工作原理是在序言中插入一个函数调用,调用运行时库。这有助于调试器区分用户代码和非用户代码。这里的问题是,将函数调用插入到项目中每个函数的序言中意味着整个项目中不再有叶函数。如果函数最初不需要任何堆栈帧,那么现在就需要了,因为按照 AMD64 ABI公司 对于Windows平台 ,我们需要在 最少的 四 可用于函数参数的堆栈插槽(称为 P 阿拉姆 家 地区 ). 这意味着之前没有被/RTCs初始化的所有函数,因为它们是叶函数 并且没有堆栈帧,现在将被初始化。在你的程序中有很多叶子函数是很正常的 , 尤其是当你使用 大量模板化的代码 图书馆 像C++ STL一样。 /JMC很乐意吃掉你的一些CPU周期 在这种情况下。这不适用于 x86个 (32位) 因为我们那里没有param home区域。 您可以看到/JMC的效果 在这里 .

与/ZI的交互

我们接下来要讨论的是 /齐。它使您的代码 编辑和 继续 支持, 也就是说你不需要重新编译 整个 在调试过程中对程序进行小改动。

为了 再加上这样的支持,我们补充道 一些 衬垫 字节 到堆栈 (这个 实际数量 属于 衬垫 字节 取决于函数的大小)。这种方式 , 在调试会话期间添加的所有新变量 可在上分配 填充区域 而不改变 全部的 堆栈帧大小,您可以继续调试,而无需重新编译代码。  看到了吗 在这里 怎样 有可能 此标志向生成的代码中添加额外的64字节。

您可能已经猜到,堆栈区域越多,意味着/RTCs要初始化的东西就越多, 导致 更高的开销。

解决方案

所有这些问题的根源是不必要的初始化。我们真的需要初始化堆栈区域吗 每一次? 不。 当 堆栈初始化 真的很需要。例如, 你需要 当至少有一个地址变量,一个数组 宣布 在你的职责范围内 或未初始化的变量。 为了 每一个 另一种情况,我们可以安全地跳过初始化, 因为我们不会通过运行时检查找到任何有用的东西 不管怎样。

情况越来越糟 一点 使用“编辑并继续”进行编译时会更复杂,因为现在可能会添加未初始化的 变量 在调试会话中,只有在初始化 堆叠区域。我们可以 不 有 我做到了。 为了解决这个问题,我们 包括 必要的 位 在里面 调试信息 并通过 调试接口访问SDK . 这些信息告诉 调试器,其中填充区域由/ZI引入 开始 末端 . 它还告诉我们 这个 调试器如果 功能 需要任何堆栈初始化吗 . 如果是的话 然后,调试器无条件地初始化此文件中的堆栈区域 记忆 调试会话期间编辑的函数的范围。新变量总是分配在这个初始化区域和我们的运行时之上 检查 现在可以检测新添加的代码是否安全。

结果

我们汇编了 下列的 默认调试配置中的项目,然后使用 生成 运行测试的可执行文件。我们注意到我们尝试的所有项目都有2到3倍的改进。更多的STL重型项目可能会看到更大的改进。让我们进来 这个 评论 任何 您在项目中注意到的改进。 项目1 和 项目 2 是客户提供的样品。

Image results

告诉我们你的想法!

我们 希望这能加速 使调试工作流程高效 而且很有趣。 我们是 连续不断地 倾听您的反馈 并致力于 改善你的内环 经验。 我们很乐意在下面的评论中听到你的经历。 你也可以和我们联系 在 开发者社区 ,电子邮件( visualcpp@microsoft.com ),和Twitter( @视觉 ).

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享