排查头文件对生成时间的影响
使用 Build Insights Include 文件和 Include 树视图,排查 C 和 C++ 生成时 #include
文件的影响。
先决条件
- Visual Studio 2022 17.8 或更高版本。
- 默认启用 C++ Build Insights,分为两种情况,一是使用 Visual Studio 安装程序安装使用 C++ 的桌面开发工作负载:
显示已安装组件的列表。 C++ Build Insights 被突出显示并被选中,这意味着它已安装。
二是安装使用 C++ 的游戏开发工作负载:
显示已安装组件的列表。 C++ Build Insights 被突出显示并被选中,这意味着它已安装。
概述
Build Insights 现已集成到 Visual Studio 中,可帮助你优化生成时间,特别是对于 3A 游戏等大型项目。 分析大型头文件,尤其是在反复分析时,对生成时间有影响。
Build Insights 在“包含的文件”视图中提供分析,这有助于诊断在项目中分析 #include
文件的影响。 它显示分析每个头文件所需的时间和头文件之间的关系视图。
本文介绍如何使用 Build Insights Included Files 和 Include Tree 视图来识别要分析的最昂贵的头文件,以及如何通过创建预编译头文件来优化生成时间。
设置生成选项
在收集 Build Insights 数据之前,请为要衡量的生成类型设置生成选项。 例如,如果担心 x64 调试生成时间,请设置 Debug 和 x64 的生成:
在“解决方案配置”下拉列表中,选择“调试”。
在“解决方案平台”下拉列表中,选择“x64”。
显示“解决方案配置”下拉列表。 它具有“调试”、“发布”和“配置管理器”的选项。 解决方案平台下拉列表设置为“x64”。
运行 Build Insights
在所选项目上,并使用上一部分中设置的“调试生成”选项,通过从项目名称>>“重新生成”上的主菜单“生成>运行生成见解”来运行 Build Insights。< 也可以右键单击解决方案资源管理器中的项目,然后选择“运行 Build Insights”>“重新生成”。 选择“重新生成”而不是“生成”来衡量整个项目(而不仅仅是当前可能存在异常的几个文件)的生成时间。
生成完成后,将打开一个事件跟踪日志 (ETL) 文件。 它保存在 Windows TEMP
环境变量指向的文件夹中。 生成的名称基于收集时间。
“包含的文件”视图
跟踪文件显示生成时间,本示例为 16.404 秒。 “诊断会话”是运行 Build Insights 会话所用的总时间。 选择“包含的文件”选项卡。
此视图显示处理 #include
文件所花费的时间。
在“文件路径”列中,突出显示了带有火焰图标的多个文件,因为它们占用了 10% 以上的生成时间进行分析。 winrtHeaders.h 占用 8.581 秒或 16.404 秒的 52.3%,它占用的生成时间最多。
在“文件路径”列中,某些文件旁边有一个火焰图标,这表示这些文件占用 10% 或更多生成时间。
“时间 [秒,%]”列显示在挂钟责任时间 (WCTR) 中编译每个函数所需的时间。 此指标根据文件使用并行线程的情况来分布分析文件所需的挂钟时间。 例如,如果两个不同的线程在一秒钟内同时分析两个不同的文件,则每个文件的 WCTR 记录为 0.5 秒。 这反映了每个文件在总编译时间中所占的比例,同时考虑到每个文件在并行执行期间消耗的资源。 因此,WCTR 可以更好地衡量每个文件在同时发生多个编译活动的环境中对整体生成时间的影响。
“分析计数”列显示分析头文件的时间。
此列表中突出显示的第一个头文件是 winrtHeaders.h
,它花费了总生成时间 16.404 秒中的 8.581 秒(即总生成时间的 52.3%)。 花费时间排第二的是 Windows.UI.Xaml.Interop.h
,然后是 Windows.Xaml.h
。
若要查看包含 winrtHeaders.h
的文件,请单击其旁边的 V 形。 “分析计数”列可通过指出头文件由其他文件包含的次数来提供帮助。 如果一个头文件被包含多次,这可能表示该头文件很适合用作预编译头文件或用于重构。
“翻译单元”列显示处理包含的文件时正在处理的文件。 在此示例中,编译时 Grapher.cpp
包含了 winrtHeaders.h
:
显示示例项目的包含文件的示例 ETL 文件。 在“文件路径”列中,选择并展开 winrtHeaders.h。 生成需要 8.219 秒,占生成时间的 50.1%。 其子节点是 Grapher.cpp,该节点也列为翻译单元。
“翻译单元”列有助于在一个头文件被多次包含并且你想要找出包含次数最多的位置时,分辨出正在编译的文件。
我们知道,分析 winrtHeaders.h
的成本高昂,但我们可以了解更多信息。
包含树视图
在此视图中,子节点是父节点包含的文件。 这有助于了解头文件之间的关系,并确定减少分析头文件次数的机会。
选择 ETL 文件中的“包含树”选项卡以查看“包含树”视图:
显示项目的包含树。 在“文件路径”列中,列出了包含其他文件的每个文件,以及这些文件包含的文件数以及分析文件的时间。
在此视图中,“文件路径”列显示包含其他文件的每个文件。 “包含计数”列出了此头文件包含的文件数。 分析此文件的时间已列出,展开后,会列出分析此头文件包含的每个单独头文件的时间。
之前我们看到分析 winrtHeaders.h
非常耗时。 在“筛选文件”文本框中,输入 winrtHeaders.h
,可以筛选视图,仅显示名称中包含 winrtHeaders.h
的条目。 单击 winrtHeaders.h
旁边的 V 形可显示它包含的文件:
“文件路径”列中列出了包含其他文件的每个文件,以及这些文件各自包含的文件数和分析文件所需的时间。winrtHeaders.h 已被选中并展开,显示它包含的文件。 Windows.UI.Xaml.Interop.h 是其中一个文件,它已展开并显示自身包含的头文件。
我们看到 winrtHeaders.h
包含 Windows.UI.Xaml.Interop.h
。 在“包含的文件”视图中,这也是分析所需的时间。 单击 Windows.UI.Xaml.Interop.h
旁边的 V 形,可以看到它包含 Windows.UI.Xaml.h
,后者包括 21 个其他头文件,其中两个文件也位于高占用列表中。
确定要分析的一些最耗时的头文件,并看到 winrtHeaders.h
负责引入它们,这表示我们可以使用预编译标头来加快添加 winrtHeaders.h
的速度。
使用预编译标头优化生成时间
我们从“包含的文件”视图中知道分析 winrtHeaders.h
非常耗时,并且从“包含树”视图中知道 winrtHeaders.h
包含分析耗时的其他几个头文件,因此我们构建了预编译头文件 (PCH),以便通过仅将它们分析到 PCH 中一次,加快分析速度。
我们添加一个 pch.h
来包含 winrtHeaders.h
,如下所示:
#ifndef CALC_PCH
#define CALC_PCH
#include <winrtHeaders.h>
#endif // CALC_PCH
PCH 文件必须先经过编译才能使用,因此我们向项目添加一个文件,给它任意命名为 pch.cpp
,该文件包括 pch.h
。 它包含一行:
#include "pch.h"
然后,我们将项目设置为使用 PCH。 方法是在项目属性中,前往“C/C++”>“预编译标头”,将“预编译标头”设置为“使用 (/Yu)”并将“预编译头文件”设置为“pch.h”。
“预编译标头”设置为“使用 (/Yu)”。 “预编译头文件”设置为“pch.h”。
若要使用 PCH,我们将它添加为使用 winrtHeaders.h
的源文件中的第一行。 它必须位于任何其他包含文件之前。 或者,为简便起见,我们可以将项目属性修改为在解决方案的每个文件开头添加 pch.h
,方法是按如下设置项目属性:“C/C++”>“高级”>“强制包含文件”设置为 pch.h
:
“强制包含文件”设置为“pch.h”。
由于 PCH 包含 winrtHeaders.h
,我们可以从当前包含它的所有文件中删除 winrtHeaders.h
。 这不是绝对必要的,因为编译器发现已包含 winrtHeaders.h
,并且不会再次分析它。 一些开发者更愿意在源文件中保留 #include
,有时是为了更清楚,有时是在 PCH 可能会重构,并且可能不再包含该头文件的情况下。
测试更改
我们首先清理项目,确保我们将与之前生成的相同文件进行比较。 若要仅清理一个项目,请在“解决方案资源管理器”中右键单击该项目,然后选择“仅项目”>“仅清除 <prj name>”。
由于此项目现在使用预编译标头 (PCH),因此我们不想测量生成 PCH 所用的时间,因为这只发生一次。 我们通过加载 pch.cpp
文件并选择“Ctrl+F7”来生成该文件,以执行此操作。 还可以通过在“解决方案资源管理器”中右键单击 pch.cpp
并选择 Compile
来编译此文件。
现在,我们通过右键单击项目并选择“仅项目”>“在生成时运行生成见解”,在“解决方案资源管理器”中重新运行生成见解。 还可以右键单击“解决方案资源管理器”中的项目,然后选择“运行生成见解”>“生成”。 我们不希望在这次重新生成,因为这将重新生成 PCH,我们不想衡量该指标。 我们之前清理了项目,这意味着普通生成会编译我们想要衡量的所有项目文件。
出现 ETL 文件时,我们看到生成时间从 16.404 秒变为 6.615 秒。 将 winrtHeaders.h
放入筛选器框,系统不显示任何内容。 这是因为分析该文件所花费的时间现在可忽略,因为它被预编译标头拉取。
此示例使用预编译标头,因为它们是 C++20 之前的常见解决方案。 但是,从 C++20 开始,还有其他一些更快速、更不易出错的方法来添加头文件,例如标头单元和模块。 有关详细信息,请参阅《比较标头单元、模块和预编译标头》。
在视图之间导航
“包含的文件”和“包含树”视图都有一些导航功能:
- 双击“包含的文件”或“包含树”中的文件(或按 Enter),打开该文件的源代码。
- 右键单击头文件,在其他视图中查找该文件。 例如,在“包含文件”视图中,右键单击
winrtHeaders.h
并选择“在包含树中查找”,在“包含树”视图中查看。
或者,可以在“包含树”视图中右键单击某个文件,跳转到“包含的文件”视图中的该文件。
提示
- 可以在“文件”>“另存为”,将 ETL 文件保存到更永久的位置,以保留生成时间的记录。 然后,可以将其与将来的生成进行比较,以查看更改是否正在改善生成时间。
- 如果不小心关闭了 Build Insights 窗口,请通过在临时文件夹中找到
<dateandtime>.etl
文件将其重新打开。TEMP
Windows 环境变量提供了临时文件文件夹的路径。 - 若要使用 Windows 性能分析器 (WPA) 深入研究 Build Insights 数据,请单击 ETL 窗口右下角的“在 WPA 中打开”按钮。
- 拖动列以更改列的顺序。 例如,你可能希望将“时间”列移至第一列。 可以通过右键单击列标题并取消选择不想看到的列来隐藏列。
- “包含的文件”和“包含树”视图提供一个筛选框,用于查找你想要查找的头文件。 它针对你提供的名称执行部分匹配。
- 有时,系统报告的头文件分析时间会有所不同,具体取决于包含该头文件的文件。 这可能是由于影响标头哪些部分已展开的不同
#define
、文件缓存和其他系统因素的相互作用。 - 如果忘记了“包含的文件”或“包含树”视图尝试显示的内容,请将鼠标悬停在选项卡上,查看描述视图的工具提示。 例如,如果将鼠标悬停在“包含树”选项卡上,工具提示会显示:“该视图显示每个文件的统计信息,其中子节点是父节点包含的文件。”
- 你可能会看到(例如
Windows.h
)分析头文件所有次数的总持续时间长于整个生成持续时间的情况。 在这种情况下,标头同时在多个线程上进行分析。 如果两个线程同时花费一秒分析一个头文件,则尽管只过去一秒的挂钟时间,这也是 2 秒的生成时间。 有关详细信息,请参阅挂钟责任时间 (WCTR)。
故障排除
- 如果未显示“生成见解”窗口,请重新生成而不是生成。 如果未实际生成任何内容,则不会显示“生成见解”窗口;如果自上次生成以来没有更改任何文件,则可能属于这种情况。
- 如果你想要的头文件未显示在“包含的文件”或“包含树”视图中,则该头文件尚未生成或生成时间没有长到可列出。
另请参阅
生成见解提示和技巧
比较标头单元、模块和预编译标头
在 Visual Studio 视频中生成见解 - Pure Virtual C++ 2023
更快的 C++ 生成,已简化:时间的新度量标准
在生成时对函数内联进行故障排除
vcperf 和 Windows 性能分析器