使用 Visual Studio 编译器分析内联 C++ 函数

Profiling inlined C++ functions with Visual Studio Compiler

当编译器内联大量代码时,我如何理解 Windows 上的 C++ 分析数据? IE。我当然想测量实际获得 运行 的代码,因此根据定义,我将测量代码的优化构建。但似乎 none 我尝试的工具实际上设法解决了内联函数。

我试过Visual Studio2017 Professional和VTune 2018中的采样分析器。我尝试启用/Zo,但似乎没有任何影响。

我发现以下资源似乎表明只有 Visual Studio Ultimate 或 Premium 支持内联框架信息 - Visual Studio 2017 是否仍然如此? https://social.msdn.microsoft.com/Forums/en-US/9df15363-5aae-4f0b-a5ad-dd9939917d4c/which-functions-arent-pgo-optimized-using-profile-data?forum=vsdebug

这是一个示例代码:

#include <cmath>
#include <random>
#include <iostream>

inline double burn()
{
    std::uniform_real_distribution<double> uniform(-1E5, 1E5);
    std::default_random_engine engine;
    double s = 0;
    for (int i = 0; i < 100000000; ++i) {
        s += uniform(engine);
    }
    return s;
}

int main()
{
    std::cout << "random sum: " << burn() << '\n';
    return 0;
}

在 Release 模式下用 Visual Studio 编译它。或者在命令行上,尝试 cl /O2 /Zi /Zo /EHsc main.cpp。然后尝试使用 Visual Studio 中的 CPU Sampling Profiler 对其进行分析。你最多会看到这样的东西:

VTune 2018 在 Windows 上看起来很相似。在 Linux 上,perf 和 VTune 可以毫无问题地显示来自内联函数的帧...这个在我看来对 C++ 工具至关重要的功能真的不是 non-Premium/Ultimate Visual Studio 的一部分吗工具链? Windows 上的人如何处理?那么/Zo有什么意义呢?

编辑: 我只是尝试用 clang 编译上面的最小示例,它产生了不同但仍然不令人满意的结果?我编译了 clang 6.0.0 (t运行k),从 LLVM rev 318844 和 clang rev 318874 构建。然后我用 clang++ -std=c++17 -O2 -g main.cpp -o main.exe 和 运行 编译我的代码,生成的可执行文件带有 Sampling Profiler再次输入 Visual Studio,结果是:

所以现在我看到了burn函数,但是丢失了源文件信息。此外,uniform_real_distribution 仍未在任何地方显示。

编辑 2: 正如评论中所建议的那样,我现在也尝试了 clang-cl,其参数与上面的 cl 相同,即:clang-cl.exe /O2 /Zi /Zo /EHsc main.cpp。这产生与 clang.exe 相同的结果,但我们也得到了一些有效的源映射:

编辑 3: 我原本以为 clang 会神奇地解决这个问题。可悲的是,它没有。大多数内联框架仍然缺失:(

编辑 4: VTune 不支持使用 MSVC/PDB 构建的应用程序构建的内联框架:https://software.intel.com/en-us/forums/intel-vtune-amplifier-xe/topic/749363

我不确定我是否正确理解了您问题中描述的问题。在您的网站上,我会尝试 /Ob0 Visual C++ 编译器选项。它必须禁用内联扩展。

/Ob 编译器选项控制函数的内联扩展。后面必须跟数字 012.

0 Disables inline expansions. By default, expansion occurs at the compiler's discretion on all functions, often referred to as auto-inlining.

1 Allows expansion only of functions marked inline, __inline, or __forceinline, or in a C++ member function defined in a class declaration.

2 The default value. Allows expansion of functions marked as inline, __inline, or __forceinline, and any other function that the compiler chooses.

/Ob2 is in effect when /O1, /O2 (Minimize Size, Maximize Speed) or /Ox (Enable Most Speed Optimizations) is used.

此选项要求您使用 /O1/O2/Ox[=33= 启用优化], 或 /Og.

To set this compiler option in the Visual Studio development environment

  1. Open the project's Property Pages dialog box. For details, see Working with Project Properties.
  2. Expand Configuration Properties, C/C++, and select Optimization.
  3. Modify the Inline Function Expansion property.

有关详细信息,请阅读文章 /Ob (Inline Function Expansion)

I have tried both the sampling profiler in Visual Studio 2017 Professional as well as VTune 2018. I have tried to enable /Zo, but it does not seem to have any affect.

I have found the following resource which seems to indicate that only Visual Studio Ultimate or Premium support inline frame information - is this still true for Visual Studio 2017?

幸运的是,我已经安装了三个不同版本的VS。我可以告诉您有关内联函数信息功能支持的更多信息,如您引用的 article 中所述:

  • VS Community 2013 Update 5 不支持显示内联函数,即使我指定了 /d2Zi+。好像只有 VS 2013 Premium 或 Ultimate 支持。
  • VS Community 2015 Update 3 支持显示内联函数(article 中讨论的功能)。默认情况下,指定 /Zi。 /Zo 通过 /Zi 隐式启用,因此您不必明确指定它。因此,您不需要 VS 2015 Premium 或 Ultimate。
  • VS Community 2017 最新更新不支持显示内联函数,无论 /Zi 和 /Zo。好像只有VS 2017 Professional支持and/or Enterprise。

VC++ 博客上没有关于 VS 2017 采样分析器的任何改进的公告,所以我认为它与 VS Community 2015 的分析器相比没有更好。

请注意,不同版本的编译器可能会做出不同的优化决策。例如,我观察到 VS 2013 和 2015 没有内联 burn 函数。

通过使用 VS Community 2015 Update 3,我得到的分析结果与 third picture 中显示的非常相似,并且突出显示了相同的代码。

现在我将讨论这些附加信息在解释分析结果时有何用处,如何通过更多努力手动获得这些信息,以及如何在有内联函数的情况下解释结果。

How can I make sense of C++ profiling data on Windows, when a lot of code gets inlined by the compiler?

VS 探查器只会将成本归因于未内联的函数。对于内联的函数,成本将被加起来并包含在一些未内联的调用函数中(在这种情况下,burn 函数)。

通过将 burn 中非内联调用函数的估计执行时间相加(如图所示),我们得到 31.3 + 22.7 + 4.7 + 1.1 = 59.8%。此外,如图所示,Function Body 的估计执行时间为 40.2%。请注意,59.8% + 40.2% = 100% 的时间花费在 burn,这是应该的。换句话说,burn 中 40.2% 的时间花费在函数体和其中内联的任何函数上。

40.2% 很多。下一个合乎逻辑的问题是,burn 中内联了哪些函数?通过使用我之前讨论的功能(在 VS Community 2015 中可用),我可以确定以下功能已内联 burn:

std::mersenne_twister_engine<unsigned int,32,624,397,31,2567483615,11,4294967295,7,2636928640,15,4022730752,18,1812433253>::{ctor};
std::mersenne_twister<unsigned int,32,624,397,31,2567483615,11,7,2636928640,15,4022730752,18>::{ctor};
std::mersenne_twister<unsigned int,32,624,397,31,2567483615,11,7,2636928640,15,4022730752,18>::seed;
std::uniform_real<double>::operator();
std::uniform_real<double>::_Eval;
std::generate_canonical;

如果没有该功能,您将不得不手动反汇编发出的可执行二进制文件(使用 VS 调试器或使用 dumpbin)并找到所有 x86 call 指令。通过将其与源代码中调用的函数进行比较,您可以确定哪些函数被内联。

直到并包括 VS 2017 的 VS 采样分析器的功能到此结束。但这确实不是一个重要的限制。通常,由于编译器对每个函数的大小强加了硬性上限,因此在同一个函数中内联的函数并不多。因此,通常可以手动检查源代码 and/or 每个内联函数的汇编代码,看看该代码是否会对执行时间产生重大影响。我这样做了,很可能 burn 的主体(不包括内联函数)和这两个内联函数主要负责那 40.2%。

std::mersenne_twister<unsigned int,32,624,397,31,2567483615,11,7,2636928640,15,4022730752,18>::seed;
std::uniform_real<double>::_Eval;

考虑到所有这些,我在这里看到的唯一潜在优化机会是记住 log2 的结果。

VTune 采样分析器肯定比 VS 采样分析器更强大。特别是,VTune 将成本归因于单个源代码行或汇编指令。然而,这种归因是高度近似的并且通常是荒谬的。因此,在解释以这种方式可视化的结果时,我会非常小心。我不确定 VTune 是否支持 Enhance Optimized Debugging information or to what degree it supports attributing costs to inlined functions. The best place to ask these questions is the Intel VTune Amplifier community forum.