Hi!请登陆

VS2019更新:更快的C++工程迭代构建

2021-1-16 16 1/16

不变的主题:快,更快,再快

在VS2019的早期版本,我们曾经对C++工程的链接时间做了一系列的优化,现在,关于性能优化这个话题,我们有更多的信息想和你分享。

在VS2019 v16.7中,我们测试发现,在某些增量链接场景下,链接速度提升了5倍之多,在调试场景下,则观测到了高达1.5倍的完整链接速度提升。在之前的一篇关于《战争机器5》开发团队的一篇文章中,会有关于此信息的更多内容,欢迎观看。

在v16.0和v16.2中进行了链接性能优化之后,我们回顾并重新评估了C++开发者的”编辑-构建-调试”的完整流程。我们考察了诸如3A游戏大作和Chrome之类的大型C++工程,因为这些大型C++工程通常会对工程构建时间十分敏感(想象一下:做一次完整编译需要花上个大半天的时间)。

首先,在VS2019 v16.6中,我们对PDB(Program Database)和DIA(Debug Interface Access)这两个组件中的算法进行了某种优化措施,从而实现调试信息的读取和写入能同

时进行。

其次,在VS2019 v16.7中,我们引入了对最坏情况下的增量编译性能的速度提升,在优化前,它可能比一次完整编译还要慢。

更快的调试信息读取

PDB(Program Database)的构建通常会是连接大型库时的一个性能瓶颈,特别是在极端执行路径下会造成十分漫长的执行时间。而且,PDB信息的读取也会显著地减缓大型工程的调试。特别是当开发者在Visual Studio打开了多个窗口,例如调用堆栈和变量监视,断点触发和单步执行时会看到明显的性能降级。

在我们内部的性能测试中,我们所做的优化对大型工程,例如3A游戏大作等,产生了显著的性能提升,下图可以作为一个例子来参考性能的对比:

虽然,在上图中,时间的绝对值差异来自于我们测试的多个不同工程。但是我们还是可以看到,多个不同的工程编译速度测试都体现了各项操作的性能提升,这些工程并没有进行择优选择,而是一些普遍选择的结果集合。结论就是:

1) 完整编译速度提升达1.5倍

2) 调用堆栈中对不同的函数进行切换速度提升了4倍

3) 初次PDB加载提升了2倍

更加令人信服的一项测试结果是,自从Visual Studio v16.6发布后,单步执行后的断点触发时间较之前快了2倍(平均值)。实际的提升效果取决于你的工程大小和当前打开的调试窗口(例如,监视窗口,调用堆栈等)的数目,但是好消息是,在之前的VS版本中碰到的单步执行缓慢的现象,在v16.6版本中可以明显地感受到我们引入的优化措施。

我们做了什么?

在开发v16.6的时候,我们重点选取了一些典型的开发场景,并找到了优化调试信息读取和写入性能的方法,下面是我们优化算法的一些详细细节:

1. 通过将之前之前的请求缓存起来,来避免通过RVA(Ralative Virtual Address)来进行查询,因为我们观察到,大概有99%的案例中使用了相同的RVA。

2. 根据需要对类型记录计算旧的CRC32哈希值。(此方法在使用了/Zi编译选项的情况下性能提升最大)

3. 对VS调试器查询模式创建一个快速执行路径。

4. 通过使用AVX指令集优化了memcpy函数,并使用优化后的memcpy来优化内存映射文件的读取过程。

5. 使用C++ strd::sort,而不是qsort。

6. 在整数除法中,使用了一个常量作为被除数(例如页面大小),而不是使用变量。

7. 重用哈希表,而不是每次都重新构建哈希表。

8. 避免虚函数调用,并且对两种最为常见的符号查询过程进行了手动内联优化。

请注意,在上面的第一种情况中,通过将之前的请求缓存起来,极大地优化了PDB的读取。

对最坏情况的优化

增量链接是我们工具中最省时的功能之一。

它允许开发人员在大型项目中进行通用源代码更改时快速进行迭代,方法是重用早期链接的大部分结果,并策略性地应用上次源代码编辑中所做的更改。

但是,它不能容纳所有源代码的更改,有时会被迫退回到完全链接,这意味着总的增量链接时间实际上可能比完全链接差

有意义的是,影响较大的编辑(例如更改编译器或链接器选项或修改被广泛包含的头文件)需要重新构建,但是仅添加新的对象(.obj)文件也将触发完全重新链接。

对于许多开发人员来说,这并不是什么大问题,因为他们很少添加新的目标文件,或者完全链接的时间反正也不会很长。

但是,如果你使用大型二进制文件,或者使用的编码风格或项目系统(例如Unity构建的某些变体)通常会导致添加或删除目标文件,则对增量链接时间的影响可能是数十秒甚至更长。

不幸的是,这些限制对于增量链接的设计至关重要,而删除它们将意味着减慢针对增量链接进行优化的最常见情况:对少量现有编译单元进行简单的源代码编辑。

缓存技术的应用

在v16.7版本中,尽管在更多情况下我们无法合理地进行增量链接,但我们意识到,可以缩短必须完全链接时的链接时间。下面是具体的考虑:

1. 完整链接的大部分时间都花在了生成调试信息上。

2. 与正确链接可执行二进制文件相比,生成正确的调试信息要宽容(容许它出现一些小错误)得多。

在概念上,类似于增量链接的工作方式,我们增加了缓存早期调试信息生成的结果(特别是类型合并的结果)并在后续链接中重复使用的功能。

当增量链接退回到完全链接时,此技术可能意味着链接时间大幅提高(2X-5X)。

下表列出了对三个3A游戏大作和Chrome工程编译产生影响的例子:

但是,使用缓存计算也存在一些缺点:

1. 缓存的数据存储在PDB文件中,因此PDB文件会更大。

2. 由于必须构建缓存,因此增量构建的第一个(干净的)链接会花费更长的时间。

下表列出了对上述工程编译产生的优点和缺点。

“Subsequent full link time”列对应于以下情形:启用了增量链接(/ INCREMENTAL),但必须回退到完全链接,例如引入新的目标文件时。如你所见,如果完整链接时间需要数十秒或几分钟时,此新缓存的影响可能很大。

有趣的是,缓存可用于任何完整链接方案,而不仅仅是增量链接必须回退到完整链接的情况。但是,由于这些缺点,只有在使用增量链接时默认情况下才启用。除非指定了新的/PDBTMCACHE链接器开关,否则发行版本和禁用增量链接(/ INCREMENTAL:NO)的版本都不会受到影响。

同样,/PDBTMCACHE:NO开关可用于禁用高速缓存创建,并根据需要返回到版本v16.6的行为。请注意,链接器不依赖于缓存的存在。如果存在缓存并通过验证,则链接器将使用它来加速链接,但是将忽略丢失的缓存或已失效的缓存。

接下来的工作

我们知道,可能有些人会担心类型合并缓存对PDB大小的影响,因此,将来,我们可能会考虑将缓存放在单独的文件中。 我们没有将其放在增量链接文件(.ilk)中,因为该功能从根本上不与增量链接相关联,这就是为什么要对它们进行独立控制的原因。

后面我们还将继续分享有关v16.8版本中对链接时间改进的一些细节。

总结

所以,总结一下:在最近的VS新版本中,我们改善了PDB文件的读写性能和优化了增量编译失败后的完整编译过程。

你是否有不一样的感受呢?

最后

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

本文来自:《Faster C++ Iteration Builds》

相关推荐