首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

使用C+Build Insights对模板代码进行性能分析

C++中的模板编程

在C++程序中使用模板有时会导致很长的编译时间。C++ Build Insights可以提供一些工具,用来分析模板使用模式(template usage patterns)及其对编译时间的相关影响。

在本文中,我们将演示如何使用vcperf分析工具和C++ Build Insights SDK来理解和修复有问题的模板模式。

我们通过一个案例研究证明了这些工具的实际使用,在该案例中,Sprout开源元编程库的编译时间减少了25%。我们希望本文中介绍的这些方法可以帮助你放心地进行C++模板代码开发。

如何获取和使用vcperf

在本文中的示例程序中,我们使用了vcperf这个工具。它可以采集编译过程中产生的各种有用的信息,并可以在Windows Performance Analyzer(WPA)中进行可视化地查看。

最新版本的vcperf包含在Visual Studio 2019中。

1. 获取和配置vcperf和WPA的步骤如下:

1) 下载并安装最新版本的Visual Studio 2019。

2) 下载并安装最新版本的Windows ADK,WPA工具包含在这个ADK中。

3) 从Visual Studio的MSVC安装目录下拷贝perf_msvcbuildinsights.dll到新安装的WPA目录下。这个文件是WPA的C++ Build Insights扩展,主要用来在WPA中正确地显示C++ Build Insights事件信息。

a. MSVC安装目录:C:\Program Files (x86)\Microsoft Visual Studio\2019\\VC\Tools\MSVC\\bin\Hostx64\x64

b. WPA安装目录:C:\Program Files (x86)\Windows Kits\10\Windows Performance Toolkit4) 在WPA目录下打开perfcore.ini文件并添加一个对应perf_msvcbuildinsights.dll的条目。这样WPA就会在其启动的时候加载C++ Build Insights扩展。

2. 采集工程编译信息的步骤如下:

1) 以管理员权限打开x64 Native Tools Command Prompt for VS 2019。

2) 采集工程编译信息

1. 执行命令:vcperf /start /level3 MySessionName。[/level3]选项表示启用模板事件采集。

2. 编译你的C++工程,可以直接在Visual Studio中编译(因为vcperf将在整个系统范围仅限信息采集)。

3. 执行命令:vcperf /stop /templates MySessionName outputFile.etl。这条命令将会停止信息采集,同时会分析所有事件信息,包括模板事件,然后保存所有的分析结果到outputFile.etl这个文件中。

在WPA中查看模板信息

在工程编译过程中,与模板有关的最耗时的活动是模板的实例化。C++BuildInsights提供了一个名为[模板实例化]的WPA视图,通过这个视图,我们可以查看程序中耗时最长的模板实例化时间。

在WPA中打开跟踪后,可以通过将其从[图形资源管理器]窗格拖到[分析]窗口中来打开该视图,如下图所示:

关于vcperf跟踪和WPA中模板事件的说明

如果在[图形资源管理器]窗格中看不到[模板实例化]视图,请确保已正确完成[如何获取和使用vcperf]部分中的WPA配置步骤,并且在启动和停止时已将正确的参数传递给vcperf。

出于扩展性方面的考虑,vcperf只会在分析结果文件中记录最耗时的模板实例化活动。如果模板实例化不是编译时间的重要因素,则vcperf将忽略模板信息,并且不会出现[模板实例化]视图。

案例研究:加快模板元编程库Sprout的编译时间

在本案例研究中,我们以一个真实开源项目为例,展示如何使用vcperf和WPA在模板元编程代码中诊断和处理较长的编译时间。具体来说,我们演示了如何使用这些工具将Sprout库的编译时间减少了约25%。

以下是具体的操作步骤:

1. 克隆Sprout工程到本机。

2. Checkout代码版本:6b5addba9face0a。

3. 获取一份Sprout完整编译的信息:

1) 以管理员权限打开x64 Native Tools Command Prompt for VS 2019。

2)执行命令:vcperf /start /level3 Sprout。

3) 执行编译命令:cl /std:c++latest /D_HAS_DEPRECATED_IS_LITERAL_TYPE=1 /D_SILENCE_CXX17_IS_LITERAL_TYPE_DEPRECATION_WARNING /EHsc /I. /constexpr:steps100000000 .\testspr\sprout.cpp4) 执行命令:vcperf /stop /templates Sprout sprout.etl。这条命令将会将采集到的编译信息保存到信息跟踪文件。

4. 在WPA中打开信息跟踪文件。

我们打开[构建资源管理器]和[模板实例化]视图。[构建资源管理器]视图指示编译持续了大约13.5秒,这可以通过查看视图底部的时间轴(标记为A)来看出来。[模板实例化]视图显示了在时间8到10.5(标记为B)之间某个位置的模板实例化活动。

默认情况下,所有模板的特化都按主模板的名称进行分组。例如,std::vector和std::vector的特化都将归类到std::vector主模板下。

在我们的案例研究中,我们想知道是否存在一种可能出问题的模板特化,因此我们重新组织了视图的列,以便按[特化名称]对列表进行分组。具体操作如下所示:

我们注意到,sprout::tpp::all_of的模板实例化大约需要2.15秒。另外,还发现sprout::tpp::detail::all_of_impl这个模板有多达511次实例化。因此,我们推测:sprout::tpp::all_of是sprout::tpp::detail::all_of_impl递归调用的根本原因。分析结果如下图所示:

代码分析

通过对代码的分析,我们发现在源文件sprout\random\shuffle_order.hpp中,对以下类型的operator()调用触发了对sprout::tpp::all_of的实例化。typedef sprout::random::shuffle_order_engine knuth_b;

这个类型在内部包含一个256个元素的数组,最终将其传递到sprout\container\container_construct_traits.hpp头文件中的default_remake_container函数。

该函数具有以下三个模板定义。为了简单起见,模板定义代码已替换为简单的注释。

这些定义全部使用std::enable_if标准类型traits类来实现在特定条件启用或禁用。你可以看出来在第二个定义的std :: enable_if条件中发现对sprout::tpp::all_of的调用吗? 下面我们来复制它:

!(sizeof…(Args) == 2 &&sprout::tpp::all_of…>::value)

从总体上看,如果调用default_remake_container时使用的参数数量不是2,则无需对sprout::tpp::all_of进行求值。在我们的例子中,我们有256个参数,并且知道无论sprout::tpp::all_of返回什么条件,该条件都将为false。

在编译器看来,这无关紧要。它在解析对default_remake_container的调用时,还是会尝试在我们的256个参数上对sprout::tpp::all_of进行求值,从而导致非常耗时的递归模板实例化。

寻找解决方法

我们通过在default_remake_container和sprout::tpp::all_of调用之间添加一个间接级别来解决这种情况。我们首先讨论参数的数量:

仅当确认参数计数为2时,我们才通过名为default_remake_container_two_args的新函数来对sprout::tpp::all_of进行求值:

评估最终结果

经过上面的代码修改之后,我们再次编译工程并收集编译信息。我们注意到编译时间减少了约25%,总计约9.7秒。[模板实例化视图]也消失了,这说明模板实例化是影响编译时间的主要因素,简而言之:我们胜利了!

使用C++ Build Insights SDK定位模板实例化问题

在使用模板元编程的代码库中,递归,耗时的模板实例化并不是一个普遍的问题,因此我们希望将来能够更快地识别这些问题,而不必经历启动WPA和手动检查跟踪的麻烦。幸运的是,大多数使用vcperf和WPA手动执行的分析任务也可以使用C++ Build Insights SDK以编程方式执行。

为了说明这一点,我们准备了RecursiveTemplateInspector SDK示例。它打印出构建中最耗时的模板实例化层次结构,以及有关它们的统计信息,例如递归树深度,实例化总数和模板特化名称。

让我们重复上一节中的Sprout案例研究,但是这次使用RecursiveTemplateInspector这个示例工程。具体操作步骤如下:

1. 克隆C++ Build Insights SDK示例工程。

2. 打开Sample.Sln并选择对应的目标平台(x86/x64)和配置(Debug/Release)进行编译。编译出来的二进制文件将位于:out///RecursiveTemplateInspector

3. 使用上面案例研究中的步骤进行编译信息收集。这一次,使用vcperf /stopnoanalyze Sprout sprout-raw.etl命令来停止收集,而不是使用/stop选项。

4. 将sprout-raw.etl作为第一个参数传递给RecursiveTemplateInspector二进制文件。

如下图所示,RecursiveTemplateInspector正确地识别出导致我们出现问题的sprout::tpp::all_of模板实例化,该模板实例递归地触发其他实例化,总共4043个实例化。

在经过修改后的代码上重新运行RecursiveTemplateInspector会显示所有有问题的模板实例均已消失,剩下的持续时间很短,可以忽略不计。

理解示例代码

首先,通过要求C++BuildInsightsSDK将我们需要的内容转发给OnTemplateRecursionTreeBranch和OnSymbolName函数,来过滤所有停止活动和简单事件。

函数的名称对C++BuildInsightsSDK如何过滤事件没有影响。只有它们的参数很重要。具体代码如下:

我们使用OnTemplateRecursionTreeBranch函数来一一捕获模板实例化递归树的每个分支。因为C++BuildInsights事件以堆栈表示,所以捕获事件之间的递归关系是一件很容易的事情。TemplateInstantiationGroup捕获类自动展开事件堆栈,并将解析线程中发生的所有模板实例化显示为从根到叶有序的类似矢量的C++容器。

因为我们将OnTemplateRecursionTreeBranch函数绑定到了stop活动事件,所以我们将始终在解析线程从最深层返回的点处接收给定递归树中的分支。

我们利用这一事实来计算递归树的所有分支时的最大深度。一旦根实例化本身达到其停止事件,我们就通过存储树的总实例化时间以及发生它的转换单元来包装实例化树。

模板实例化事件不包含实例化的符号的名称。派生名称是一项昂贵的操作,而在测量实例化时这样做会扭曲时间测量。而是发出了一个数字键,稍后我们可以通过侦听SymboName事件将其与适当的名称匹配。OnSymbolName函数就是这样做的,并存储每个根模板实例化的名称。

在分析的最后,我们遍历所有根模板实例,按最长持续时间对其进行排序,然后显示它们。

总结

祝今日长文阅读愉快。

  • 发表于:
  • 原文链接https://kuaibao.qq.com/s/20200531A0I7AM00?refer=cp_1026
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券