使用示例配置文件引导式优化(SPGO)提高 C++ 性能

基于配置文件引导的优化(PGO)使用运行时数据来帮助编译器做出更好的优化决策。 通过使用从代表性工作负荷收集的执行配置文件数据,PGO 使编译器能够更智能地决定内联、代码布局和热/冷代码分离。 这些决策无法单独从静态分析中做出。

SPGO 采用不同的方法。 SPGO 使用从你实际发布的二进制文件中收集的硬件性能计数器采样,而不是对二进制文件进行插桩并让其在合成训练场景中运行。 新式处理器提供硬件采样功能。 你可以以几乎可忽略的运行时开销收集这些样本,因此能够直接从生产代码中收集运行时分析数据。

由于 SPGO 配置文件发布位而不是经过检测的生成,因此它可以在收集数据的位置和方式方面具有更大的灵活性。 可以从生产服务器、开发人员计算机、性能实验室或任何组合收集运行时配置文件。 其结果是生成一个二进制可执行文件,使热点路径运行得更高效,通常可带来 5%–15% 的性能提升,具体取决于性能分析数据的质量。

在本教程中,你将逐步完成完整的 SPGO 工作流:构建一个示例应用,使用 xperf 对其进行性能分析,准备性能分析数据,并使用这些数据重新构建应用。 完成后,可以将相同的过程应用于自己的项目。

先决条件

在开始之前,请确保具有以下软件和硬件。

Software

  • 适用于 x64/x86/ARM64 v14.51 或更高版本的 MSVC 生成工具 - 通过Visual Studio安装程序安装它们。 在 “单个组件”下,搜索“MSVC 生成工具”。
  • Windows Performance Toolkit(xperf.exe)xperf探查器在程序执行期间收集示例数据。 从 ADK install0 下载Windows评估和部署工具包(ADK)。 运行 ADK 安装程序时,选择 Windows Performance Toolkit 组件以获取 xperf。 无需安装完整的 ADK。
  • 战争与和平文本文件 - 用作生成分析数据的示例工作负荷。 从古腾堡计划下载:https://www.gutenberg.org/ebooks/2600。 将其另存为工作目录中的纯文本文件。

硬件要求

本教程有三个分析路径。 使用的路径取决于硬件。 在 “选择分析方法 ”中运行检测命令,找出计算机支持的路径。 目前,使用此表确认你至少满足其中一项要求。

Path CPU 要求 备注
LBR (最佳结果) 最后分支记录(LBR)是由 Intel Haswell CPU(第 4 代 Core,2013 年)及后续版本、AMD Zen 4(2022 年)及后续版本以及 ARM64 ARMv9.2-A(2020 年)及后续版本提供的性能计数器。 提供最优的分支数据。 有关 LBR 的详细信息,请参阅 最后一个分支记录简介
PMC/IP 模式 (良好结果) 任何配备性能监控单元(PMU)的 x64 CPU 都支持性能监控计数器(PMC) 可在大多数不支持 LBR 的现代 CPU 上运行。 有关 PMC 的详细信息,请参阅 记录硬件性能(PMU)事件记录硬件性能(PMU)事件以及完整示例
OS 计时器(在任何地方都能工作) 任何 x64 或 ARM64 CPU,包括Azure VM 和虚拟机 保真度较低的样本,但始终可用

大多数新式 x64 桌面硬件上的开发人员都支持 LBR。 VM 和某些较旧的硬件具有 PMC 或 OS 计时器。

SPGO 的工作原理

SPGO 从正在运行的二进制文件收集配置文件数据,并在下次生成时将其馈送回编译器。 编译器使用该数据对内联、代码布局和分支预测做出更好的决策。 一个便利之处是无需进行插桩。

工作流为:

  1. 使用 /spgo 链接器标志生成二进制文件。 此步骤将创建一个空的示例配置文件数据库.spd 文件)。
  2. 使用 xperf 分析二进制文件以生成 ETL 跟踪文件。
  3. 使用 SPTAggregate.exe 将 ETL 转换为 SPT 文件,然后使用 SPDConvert.exe 将 SPT 转换为 SPD 文件。
  4. 使用指向已填充样本配置文件数据库 (SPD) 的 /spdin 链接器标志重新构建。 链接器执行 SPGO 优化。

优化器使用 SPD 回答以下问题:最常采用哪些分支? 在热循环中调用哪些函数? 此过程生成的代码布局和内联决策比仅静态分析更好。

SPGO 适用于 C 和 C++ 。 这两种语言的工作流和标志相同。

最适合 SPGO 的对象: 包含大量分支且具有紧凑内层循环的大型 C/C++ 应用程序。 收益会随着代码库规模和分支复杂性的增加而扩大。 本教程中的这个小示例显示了大约 7% 的提升。 较大的生产代码库通常会看到更多改进。

构建过程比较

如果你想了解其工作原理,本节将介绍 SPGO 如何融入构建流水线。

常规生成过程

在标准的 C/C++ 发布版本中:

  • 输入:源代码文件(.cpp.h)和发布模式编译器标志(/O2/GL等等)。
  • 过程: 编译器仅基于静态分析应用标准优化,例如内联启发式、分支预测假设和代码布局决策。 它没有有关程序在运行时实际行为的数据。
  • 输出: 可执行文件(.exe)、DLL 文件(.dll)、调试信息(.pdb)。

常规发布版本构建过程的示意图,其中显示源代码文件和示例编译器开关 /GL 作为输入流入构建步骤,并生成 .exe、.dll 和 .pdb 输出。

如果没有运行时数据,热路径和冷路径会收到类似的处理。

已启用 SPGO 的构建过程

SPGO 将性能分析数据添加为构建管道的新输入:

  • 输入:源代码、.spd 性能分析配置文件(性能分析运行中的样本计数)、发布模式编译器标志、/link /spgo/spdin:<path>,用于指定输入 SPD 文件(如果未指定,则默认使用位于 obj 文件夹中、以二进制文件名命名的 .spd 文件)。
  • 过程: 链接器同时读取 SPD 和中间代码。 它利用分支频率数据,在内联、代码布局和分支顺序方面做出更优的决策。 热函数布局为快速访问;冷代码移出关键路径。
  • 输出:优化的可执行文件()、优化的 DLL 文件(.exe.dll)、调试信息(.pdb)和用于将来分析迭代的新.spd文件

启用 SPGO 的生成过程示意图,显示源代码和性能分析数据文件 (.spd) 作为生成步骤的输入,并使用额外的链接器开关 /spgo。生成过程会输出优化后的 .exe、.dll 文件、调试信息 (.pdb) 以及新的性能分析数据文件 (.spd)。

关键洞见:SPGO 将优化决策从依赖编译器和链接器启发式规则,转向基于真实执行数据的数据驱动决策。

关键标志

Flag 类型 Purpose
/spgo Linker 启用 SPGO。 在二进制文件中嵌入 SPGO 元数据并创建空 .spd 输出文件,除非 /spdin 指定,在这种情况下,指定 .spd 文件用作输入。
/spdin:<path> Linker 输入 SPD - 向链接器提供配置文件数据以优化
/spd:<path> Linker 输出 SPD 路径 - 指定新 SPD 写入的位置(可选;默认为与二进制文件相同的目录)。 如果未指定 /spdin,则将其用作输入 SPD 路径。
/GL 编译器 SPGO 跨翻译单元工作所需的全程序优化
/O1/O2 (最小化大小、最大化速度) 编译器 针对速度进行优化;启用 SPGO 可以增强的激进优化

SPGO 与 PGO 有何不同

PGO(配置文件引导优化)要求先使用插桩标志编译二进制文件(/GENPROFILE),运行速度较慢的插桩后二进制文件以收集 .pgc 执行计数文件,然后使用 /USEPROFILE 重新链接。 编译器获取确切的执行计数,但必须先检测代码。 有关此过程的详细信息,请参阅 配置文件引导式优化

SPGO 使用硬件 CPU 性能计数器从未经插桩的发布版二进制文件中收集统计样本。 运行现有的二进制文件,使用 xperf 对其进行性能分析,将跟踪数据转换为 SPD 文件,然后重新构建。 无需插桩构建,性能分析期间也不会变慢。 编译器获取统计采样数据,而不是精确计数,这不太精确,但更容易获取,不需要更改代码。 它还允许分析难以使用检测方法收集数据的系统组件或实时组件。 您还可以对最终版/发布版二进制文件进行性能分析。

本教程介绍三种分析方法:LBR、PMC 和 OS 计时器。 您可以在 “选择性能分析方法” 中选择性能分析方法。 有关常规生成过程与 SPGO 生成过程的详细比较,包括标志引用表,请参阅 生成过程比较

配置 perfcore.ini

⚠️ 必需: 如果没有此步骤, xperf 则不提供必要的分析数据。 在运行 xperf之前完成此步骤。

Windows Performance Toolkit (WPT) 使用 perfcore.ini 来注册 SPGO 所需的 DLL 提供程序;如果 WPT 安装在默认位置,则该文件位于 C:\Program Files (x86)\Windows Kits\10\Windows Performance Toolkit\perfcore.ini

以管理员身份打开Windows记事本。 然后打开 perfcore.ini。 找到 DLL 列表部分并添加以下条目,每行一个:

perf_spt.dll
perf_lbr.dll

如果尚未安装 xperf.exe,请参阅 常规问题 了解如何安装它。

保存并关闭 perfcore.ini。 DLL 文件已寄送到同一目录中 xperf.exe ,因此无需将其复制到任何位置。 你只是在 perfcore.ini 中注册它们。 确保 xperf 在您的路径中。

创建示例应用

本教程的示例应用是一个 C++ 程序,它从标准输入中读取文本,并生成行数、字数计数、总字符计数、字符频率表以及处理文件的运行时间(以毫秒为单位)。 它以 C++ 编写,但 SPGO 也适用于 C。C 项目的工作流完全相同。

创建工作目录中命名 textCount.cpp 的文件,并添加以下源代码:

// textCount.cpp : Text Statistics Counter
// Counts words, lines, and character frequencies from standard input
// Usage: textCount < file.txt

#include <iostream>
#include <string>
#include <map>
#include <cctype>
#include <chrono>

int main()
{
    auto start = std::chrono::steady_clock::now();

    std::map<unsigned char, int> charFrequency;
    int wordCount = 0;
    int lineCount = 0;
    int totalChars = 0;

    std::string line;
    bool inWord = false;

    while (std::getline(std::cin, line))
    {
        lineCount++;

        for (char c : line)
        {
            totalChars++;
            unsigned char uc = static_cast<unsigned char>(c);
            charFrequency[uc]++;

            if (std::isspace(static_cast<unsigned char>(c)))
            {
                inWord = false;
            }
            else
            {
                if (!inWord)
                {
                    wordCount++;
                    inWord = true;
                }
            }
        }

        inWord = false;
    }

    std::cout << "\n=== TEXT STATISTICS ===" << std::endl;
    std::cout << "Lines: " << lineCount << std::endl;
    std::cout << "Words: " << wordCount << std::endl;
    std::cout << "Total Characters: " << totalChars << std::endl;

    std::cout << "\n=== CHARACTER FREQUENCIES ===" << std::endl;

    std::cout << "\nLetters:" << std::endl;
    for (unsigned char ch = 'a'; ch <= 'z'; ch++)
    {
        unsigned char upperCh = static_cast<unsigned char>(std::toupper(ch));
        int count = charFrequency[ch] + charFrequency[upperCh];
        if (count > 0)
        {
            std::cout << static_cast<char>(ch) << ": " << count << std::endl;
        }
    }

    std::cout << "\nDigits:" << std::endl;
    for (unsigned char ch = '0'; ch <= '9'; ch++)
    {
        if (charFrequency[ch] > 0)
        {
            std::cout << static_cast<char>(ch) << ": " << charFrequency[ch] << std::endl;
        }
    }

    std::cout << "\nSpecial Characters:" << std::endl;
    for (const auto& pair : charFrequency)
    {
        unsigned char ch = pair.first;
        if (!std::isalnum(ch))
        {
            std::string displayChar;
            switch (ch)
            {
                case ' ': displayChar = "[space]"; break;
                case '\t': displayChar = "[tab]"; break;
                case '\n': displayChar = "[newline]"; break;
                case '\r': displayChar = "[return]"; break;
                default:
                    if (ch >= 32 && ch < 127)
                    {
                        displayChar = std::string(1, static_cast<char>(ch));
                    }
                    else
                    {
                        displayChar = "[byte:" + std::to_string(static_cast<int>(ch)) + "]";
                    }
                    break;
            }
            std::cout << displayChar << ": " << pair.second << std::endl;
        }
    }

    auto end = std::chrono::steady_clock::now();

    auto elapsed = std::chrono::duration<double, std::milli>(end - start);
    std::cout << "Elapsed time: " << std::fixed;
    std::cout.precision(3);
    std::cout << elapsed.count() << " ms\n";

    return 0;
}

生成并运行示例以获取基线

在应用 SPGO 之前,请生成 textCount,然后针对大型文本文件运行它,例如 War and Peace(可以从 Project Gutenberg 下载它),以查看其运行速度。 此步骤显示使用 SPGO 对其进行优化之前的性能:

建立:

cl /EHsc /GL /O2 textCount.cpp

运行:

textCount.exe < warAndPeace.txt

会看到类似于以下内容的输出:

=== TEXT STATISTICS ===
Lines: 66041
Words: 566333
Total Characters: 3227531

=== CHARACTER FREQUENCIES ===

Letters:
a: 202719
...

Elapsed time: 512.000 ms

请记录 Elapsed time 值。 你将在 度量结果中将其与 SPGO 优化的时间进行比较。

使用 /spgo 生成 textCount

现在在启用 SPGO 的情况下构建 textCount。 此步骤为收集分析数据奠定了基础。

cl /EHsc /GL /O2 textCount.cpp /link /debug /spgo

构建完成后,您会看到类似如下的消息:

SPD textCount.spd not found, compiling without profile guided optimizations

此消息会在首次 /spgo 构建时显示。 链接器会创建 SPD 文件,但它仍然为空,因此它尚未应用任何 SPGO 优化。 运行二进制文件、收集配置文件数据并将其转换为 SPD 后,不会看到此消息。

标志说明:

Flag Purpose
/EHsc 启用 C++ 异常处理
/GL 全程序优化——SPGO 所必需。 将最终优化延迟到链接时,从而能够进行跨模块内联、代码布局和死代码消除等决策。
/O2 针对速度进行优化 - 启用主动内联、循环优化、死代码删除和相关转换。
/link /debug /debug 传递给链接器以生成调试信息(.pdb),xperf 使用这些信息将性能分析样本映射到源代码。
/spgo SPGO 链接器标志 - 在二进制文件中嵌入 SPGO 元数据,并将空 textCount.spd 文件与可执行文件一起创建。

注释

/spgo 是链接器标志。 通过 cl 命令中的 /link /spgo 将其传递给链接器。

/spgo 标志尚不会优化二进制文件。 它将其准备好以进行性能分析。 在 SPD 填入真实的运行时数据后,系统会在 使用 /spdin 重新构建 textCount 之后进行优化。

注释

若要将 SPD 写入特定位置,请添加可选的 /spd:<path> 链接器标志。 例如: /link /debug /spgo /spd:.\profiles\textCount.spd。 如果省略此标志,SPD 将与 .exe 一同创建。

选择分析方法

SPGO 支持三种分析方法。 使用的方法取决于硬件。

三种分析方法

方法 样本质量 硬件要求 最适用于
LBR(最近分支记录) 最高 - 记录最近获取的分支的序列,为每个样本提供更丰富的控制流数据 Intel Haswell (2013) 或更高版本;AMD Zen 4 (2022) 或更高版本;ARM64 ARMv9.2-A (2020) 或更高版本 大多数新式桌面硬件
PMC /IP 模式 (性能监视计数器/指令指针模式) 好。 使用 CPU 的性能监视单元(PMU),通过 Windows 事件跟踪(ETW)收集带有调用堆栈的指令指针样本 具有 PMU 的任何 x64 或 ARM64 CPU 不支持 LBR 的硬件
OS 计时器 基础—基于定时器的示例 任何 x64 或 ARM64 CPU,以及未启用 PMU 直通的虚拟机 VM 和旧硬件

在 PMC/IP 模式下,每次硬件中断只会给出一个数据点:“中断触发时,CPU 位于地址 0x1A2B3C4D。” 使用 LBR 时,每次中断都会提供一份堆栈记录,显示 CPU 在中断触发前执行过的最近 16 到 32 次分支跳转。 优化器获取更好的控制流数据,并且可以做出更好的内联和布局决策。

检测您的路径

运行以下两个命令来确定计算机支持的分析路径。 这些命令不需要在提升的命令提示符中运行。

步骤 1:检查是否支持 LBR。 此测试适用于 Intel/AMD/ARM64。

administrator Visual Studio 开发人员命令提示符运行以下命令:

xperf.exe -on PMC_PROFILE -pmcprofile TotalIssues -LastBranch PmcInterrupt -setProfInt TotalIssues 2560000
xperf -stop -d lbrtest.etl
xperf -tle -i lbrtest.etl -a dumper | findstr "LBR,  TimeStamp"
  • 如果此命令找到包含 LBR, TimeStamp的行,则计算机支持 LBR。 使用 LBR 路径。
  • 否则,请继续执行步骤 2。

步骤 2:检查是否支持 PMC(无 LBR 支持)

xperf.exe -pmcsources | findstr TotalIssues
  • 如果此命令生成输出,则计算机支持 PMC 计数器,但不支持 LBR。 使用 PMC 路径。
  • 如果此命令不生成输出,则 使用 OS 计时器路径。

有关使用 xperf 收集 PMU 事件的详细信息,请参阅 使用 xperf 录制硬件 PMU 事件

决策表

LBR, TimeStamp 输出 TotalIssues 输出 您的路径
不为空 (未选中) 利比里亚
不为空 PMC
OS 计时器
ARM64 处理器 N/A PMC (如果 PMU 可用)或 OS 计时器

选择你的方法

根据检测结果决定是否使用 LBR、PMC 或 OS 计时器路径。 每个路径都有不同的 xperf 开始参数来收集适当的分析数据。 遵循与硬件功能匹配的路径。

路径:

所有路径都会重新汇合到 运行工作负载并停止 xperf 这一步。

本节中的命令取决于在 “选择分析方法”中标识的分析路径。 找到与路径匹配的子节,运行 start 命令,然后继续运行xperf工作负荷并停止 xperf 以运行工作负荷并停止 xperf。

⚠️ 以管理员身份运行:xperf 需要使用已提升权限的(管理员)开发人员命令提示符。 如果没有提升的权限,xperf 会返回 "failed to configure counters"

LBR 路径

使用 LBR 集合启动 xperf

xperf -on LOADER+PROC_THREAD+PMC_PROFILE -MinBuffers 4096 -MaxBuffers 4096 -BufferSize 4096 -pmcprofile BranchInstructionRetired -LastBranch PmcInterrupt -setProfInt BranchInstructionRetired 16384

参数说明:

参数 Purpose
LOADER+PROC_THREAD+PMC_PROFILE 内核提供程序:加载程序事件(模块映射)、进程/线程事件(执行上下文)和 PMC 分析事件
-MinBuffers 4096 -MaxBuffers 4096 -BufferSize 4096 使用较大的环形缓冲区,以避免在完整运行《战争与和平》期间出现样本丢失
-pmcprofile BranchInstructionRetired PMC 事件触发器:在每个已停用分支指令上生成示例
-LastBranch PmcInterrupt 启用 LBR 硬件记录:在每次 PMC 中断时,捕获硬件最后分支记录堆栈
-setProfInt BranchInstructionRetired 16384 采样间隔:每执行完 16,384 条分支指令触发一次中断

启动 xperf 后,继续 运行工作负荷并停止 xperf

PMC 路径(无 LBR)

从 PMC/IP 模式集合开始 xperf

xperf -on LOADER+PROC_THREAD+PMC_PROFILE+PROFILE -MinBuffers 4096 -BufferSize 4096 -pmcprofile InstructionRetired -setProfInt InstructionRetired 16384 -stackwalk profile

参数说明:

参数 Purpose
LOADER+PROC_THREAD+PMC_PROFILE+PROFILE 添加 PROFILE(CPU 采样)以及用于 PMC 事件的 PMC_PROFILE;没有 -LastBranch
-pmcprofile InstructionRetired PMC 事件触发器:已停用指令的示例(指令指针模式)
-setProfInt InstructionRetired 16384 每 16,384 条停用指令触发中断
-stackwalk profile 在每次性能分析中断时捕获调用堆栈,提供调用链数据,而非分支序列数据

与 LBR 相比:没有 -LastBranch 标志;使用 InstructionRetired 而不是 BranchInstructionRetired。 结果是具有调用堆栈(而不是分支序列)的指令指针示例。 此路径仍为优化器提供有效的数据,但不太丰富。

启动 xperf后,继续 运行工作负荷并停止 xperf

OS 计时器路径

使用基于 OS 计时器的采样启动 xperf:

xperf -on LOADER+PROC_THREAD+PROFILE -MinBuffers 4096 -BufferSize 4096 -setProfInt Timer 1221 -stackwalk profile

参数说明:

参数 Purpose
LOADER+PROC_THREAD+PROFILE 无 PMC 事件;仅通过操作系统计时器中断进行 CPU 采样
-setProfInt Timer 1221 在 OS 计时器上触发每 1,221 个计时器计时周期(大约 1 kHz)
-stackwalk profile 在每次计时器中断时捕获调用堆栈

与 LBR 和 PMC 相比,此方法不使用硬件性能计数器。 无论 CPU 活动如何,OS 计时器都会以大致固定的时间间隔触发。 示例与热代码不太紧密相关,但仍为优化器提供有用的控制流数据。

运行工作负载并停止 xperf(所有路径)

xperf 运行的情况下,让 textCount 对《战争与和平》运行:

textCount.exe < warAndPeace.txt

textCount 完成后,停止 xperf 并写入跟踪文件。 让其他进程在分析过程中运行会稀释样本质量。 为了获得最佳结果,在运行工作负荷之前关闭不必要的应用程序。

xperf -stop -d textCount.etl

停止 xperf 后(可能需要一段时间才能写出 etl 文件),确认 textCount.etl 已在当前目录中创建。

将 ETL 文件转换为 SPT

对于所有三个分析路径,此步骤都是相同的。

运行 SPTAggregate.exe 以处理原始 ETL 跟踪并创建 SPT 配置文件:

SPTAggregate.exe /binary textCount.exe /etl textCount.etl textCount.spt

参数说明:

参数 Purpose
/binary textCount.exe 用于从中提取样本的二进制文件。 ETL 可能包含分析期间运行的所有进程的样本
/etl textCount.etl 输入 ETL 跟踪文件
textCount.spt 输出 SPT 配置文件

SPTAggregate 输出显示收集的样本数的摘要。 此摘要是性能分析已成功的首次确认。

对照你所采用的路径检查来自 SPTAggregate 的输出:

  • LBR 路径: 查看“已使用的 LBR 样本计数”是否为非零。
  • PMC 路径: 查看是否存在非零的 PMC 计数或堆栈样本计数。
  • OS 计时器路径: 查找非零的已使用堆栈样本计数。

如果所有计数均为零,请参阅 故障排除 ,然后再继续操作。

将 SPT 文件转换为 SPD

路径:

PMC 和 OS 计时器路径都使用 /mode:IP ,因为两者都生成指令指针示例。

下一步会根据分析路径进行分支,具体取决于传递给SPDConvert.exe/mode标志。

LBR 模式

SPDConvert.exe /mode:LBR textCount.spd textCount.spt

/mode:LBR 指示 SPDConvert 将 SPT 解释为包含 LBR 分支序列数据。

IP 模式(PMC 和 OS 定时器)

PMC 和 OS 计时器都生成指令指针示例,因此两者都使用相同的转换命令:

SPDConvert.exe /mode:IP textCount.spd textCount.spt

/mode:IP 指示 SPDConvert 将 SPT 解释为包含指令指针示例。

Warning

对数据类型使用错误的模式可能会生成空或格式不正确的 SPD。 如果使用 LBR 进行分析,请使用 /mode:LBR。 如果使用 PMC 或 OS 计时器进行分析,请使用 /mode:IP。 将 SPTAggregate摘要输出显示收集的示例类型,并确认要使用的正确模式。

运行 SPDConvert后,确认 textCount.spd 已在当前目录中创建(或已更新)。

解读 SPDConvert 输出

该命令 SPDConvert textCount.spd textCount.spt 输出前和后块覆盖率摘要,例如:

Block coverage (before) : 33.90% ( 4507/ 13294)
Block coverage (after)  : 45.64% ( 6067/ 13294)

此摘要显示具有关联配置文件数据的二进制代码块的百分比。 更高的百分比更好。 超过 70 个% 的覆盖率非常出色,而覆盖率低于 40% 可能会限制优化有效性。 如果覆盖率较低,请延长性能分析工作负载的运行时间,或者合并来自使用不同工作负载的多次单独运行的多个 SPT 文件。 例如,可以针对多个文本文件运行 textCount ,以练习不同的代码路径。

你可能会看到来自 SPDConvert 的警告,如下所示:

Compiler may be conservative on some hot functions due to sparse sample coverage.
SPGO is estimated to optimize better if sample density is increased to 5.4x of current level.
Sample density can be increased by sampling for longer period, or increasing sample rate.

此警告表示,性能分析过程未收集到足够的样本,导致优化器无法有足够把握地优化所有热点函数。 SPD 仍可用,但可以通过以下方法改进结果:

  • 运行工作负荷的时间更长(例如,5 分钟或更长时间而不是 1 分钟),或使用不同的工作负荷。
  • 降低 -setProfInt 命令中的 xperf 值以提高采样率。 代价是,此更改会生成更大的 ETL 文件,因此处理时间会更长。
  • 将来自单独性能分析运行的多个 SPT 文件全部传递给 SPDConvert,以将其合并。

SPT 文件是二进制格式。 若要检查其内容,可以运行 SPTDump.exe textCount.spt。 同样, PTDump.exe textCount.spt 在运行 SPDConvert后显示已编译的配置文件数据。 在继续操作之前,这两种工具都可用于验证非零样本。

使用 /spdin 重新生成 textCount

使用填充的 SPD 文件重新生成 textCount 。 链接器读取配置文件数据并应用 SPGO 优化。

对于所有三个分析路径,此步骤都是相同的。

cl /EHsc /GL /O2 textCount.cpp /link /debug /spgo /spdin:textCount.spd

新标志(与 使用 /spgo 生成 textCount 相比):

Flag Purpose
/spdin:textCount.spd 向链接器提供 SPD 配置文件数据以优化

该命令仍然包含 /spgo。 它与优化的二进制文件一起生成新的 SPD 文件,可以将其用作后续分析迭代的起点。

Warning

SPD 文件与其所分析的确切二进制文件相关联。 如果在不使用 /spdin 的情况下重新构建 textCount,或者根据已更改的源代码重新构建 textCount,则必须生成新的 SPD 文件。 现有的 GUID 与新二进制文件的 GUID 不匹配,并且链接器不会使用它。

使用 /spdin重新生成后,链接器会输出有关使用配置文件数据优化了多少代码的统计信息。 例如:

221 of 221 (100.00%) profiled functions will be compiled for speed
201 of 1383 inline instances were from dead/cold paths
474 of 474 profiled functions (100.0%) were optimized using profile data
202738780 of 202738780 instructions (100.0%) were optimized using profile data

高百分比表示 SPD 很好地涵盖了你的二进制文件。 如果该百分比较低(例如低于 90%),要么是用于性能分析的工作负载未覆盖该二进制文件的足够部分,要么是该二进制文件自收集性能分析数据以来发生了显著变化。 在这两种情况下,针对当前二进制文件重新生成文件。

SPGO 如何处理您的个人资料数据

SPGO 使用收集到的采样数据,在程序控制流图中的每个块和边上填入计数值。 这些计数可驱动如下优化,例如:

  • 基于性能分析的内联:积极内联热点调用点,同时避免因内联冷路径而导致代码膨胀。
  • 热/冷代码分离:将很少执行的代码移到二进制文件的各个部分,从而提高指令缓存利用率和分页行为。
  • 函数布局:将经常相互调用的函数放置在二进制文件中,减少页面错误并改进位置。 优化后的函数在二进制文件中被组织到高亲和性的 COFF 组中。
  • 大小/速度取舍:对热点函数优先优化速度,对冷点函数优先优化大小。 未观察到性能分析命中的例程可能会按较小体积而非较高速度来编译,从而限制这些冷路径上的内联和循环展开等优化。
  • 推测性去虚拟化:当采样显示某个间接调用始终指向同一个函数时,SPGO 可以对该目标进行推测并将其内联,而对于少见情况则回退到后备路径。

对结果进行衡量

再次运行 textCount 并比较已用时间。

textCount.exe < warAndPeace.txt

为每个配置收集多个运行并使用中值。 单个运行不可靠,因为 OS 计划和系统噪音可能会扭曲各个度量值。

构建 代表性耗用时间
基线 (cl /EHsc /O2 (你的测量值)
/spgo 构建版本(尚无性能分析数据) (应接近基线)
经 SPGO 优化(/spdin (应显示改进)

在一次测试中,采用 LBR 方法的 SPGO 实现了约 7% 的耗时降低。 由于 SPGO 带来的收益取决于用于性能分析的工作负载对典型执行情况的代表性如何,因此在你自己的项目中,结果可能会有所不同。 规模更大、分支较多的代码库,其改进幅度往往更明显,通常在 5%–10% 之间。 分析方法会影响优化质量。 LBR 通常比 PMC 生成更好的结果,这比 OS 计时器生成更好的结果。 如果使用的是 OS 计时器路径,预期收益会更小。

本教程中遵循的 LBR 路径应用于 SQLite 项目,该项目是生产数据库库。 经过 SPGO 优化的 SQLite 二进制文件性能提升了约 7%。

将 SPGO 应用到自己的项目

使用此清单将 SPGO 应用到自己的 C 或 C++ 应用程序。

  1. /link /spgo 添加到现有的发布构建命令中。 修改生成脚本或项目文件:

    cl /EHsc /GL /O2 myapp.cpp /link /spgo
    
  2. 选择具有代表性的工作负荷。 选择一个能够覆盖应用程序热点路径的真实使用场景。 使用接近生产环境的数据。 请避免以下情况作为主要分析工作负荷:代码覆盖率测试(它们不强调性能瓶颈)、不常见的错误路径、启动和关闭阶段以及已弃用的代码路径。 此工作负载会生成供优化器使用的配置文件。

  3. 使用检测到的路径运行 xperf。 使用您在 选择您的性能分析方法 中确定的路径(LBR、PMC 或 OS 计时器)。 启动 xperf、运行工作负荷一次、停止 xperf并捕获 ETL 文件。

  4. 对于 PMC 或 OS 计时器路径, 请使用正确的 /mode 标志运行 SPTAggregate 和 SPDConvert。 将 ETL 转换为 SPT,然后转换为 SPD。 对 LBR 数据使用 /mode:LBR;对 PMC 或 OS 计时器数据使用 /mode:IP

  5. 使用 /spdin:<your-spd-path> 重新构建。 使用已填充的 SPD 编译应用程序:

    cl /EHsc /GL /O2 yourApp.cpp /link /spgo /spdin:yourApp.spd
    
  6. 前后测量。 分别使用未优化和经 SPGO 优化的二进制文件运行工作负载。 为每个配置收集 多个运行的中间值 。 单个运行对于基准测试并不可靠。

  7. .spd 文件存储在源代码管理中。.spd 文件与源代码一并提交到版本控制系统中。

  8. 在开发人员发布版本中启用 SPGO。 让您团队的发布版本使用与生产环境相同的、经过 SPGO 优化的二进制文件。 这有助于及早发现性能回退问题。

  9. 在调试版本中禁用 SPGO。

  10. 查看链接者个人资料完整度统计信息。 每次使用 /spgo 构建后,请注意使用性能分析数据优化的已分析函数所占的百分比。 如果这大幅下降(低于 90%),请重新生成当前二进制文件。 代码更改会累积,SPD 可能会过时。

替代使用 xperf 的方法

收集性能分析数据的另一种方法是使用采样分析器,例如 Windows Performance Recorder(WPR)。 WPR 默认安装在 Windows 10 及更高版本。 它收集与 xperf 类似的数据。 可以将 WPR 配置为使用调用堆栈收集 CPU 示例,然后将数据导出到可以使用 ETL 处理的 SPTAggregateSPDConvert ETL 文件(如 xperf ETL)。 下面是一个使用 WPR 收集分析数据的示例:

wpr -start CPU.light -filemode
textCount.exe < warAndPeace.txt
wpr -stop spgo_data.etl

有关使用 WPR 的详细信息,请参阅 Using Windows Performance Recorder

SPD 分布

您可以:

  • 直接将 .spd 文件与源代码一起签入到版本控制系统中。
  • 与团队成员共享.spd文件,以便他们无需重新分析性能数据即可使用 SPGO 优化进行构建。
  • .spd 文件与二进制文件打包为版本控制的项目(例如 NuGet 包),并记录哪个版本对应于哪个二进制文件。
  • 您可以随时通过重复分析工作流来重新生成 .spd 文件。

SPD 绑定到构建它时生成的完全对应的二进制文件。 在发生重大代码更改后,请重新生成新的 SPD。 在 /spdin 生成期间,编译器还会生成一个新 .spd 文件。 将此新的 SPD 保存为生成项目 - 它是下一次分析迭代的起点。

在各次构建之间重用 SPD 信息

SPGO 中的“延续”概念使你能够将性能分析数据添加到现有的 SPD 文件中,而无需从头重新对所有场景进行性能分析,也不会丢失现有的性能分析信息。 您还可以调整对较早的个人资料数据赋予多少权重。 如果一段时间内可能存在行为更改,并且不希望完全丢失早期方案运行中的分析信息,则这种灵活性非常有用。 例如,随着调用它的应用程序不断演进,DLL 可能会调用到不同的 API。 你仍然希望保留它过去行为方式所带来的优化,同时也想结合它现在有时表现不同这一情况的优化机会。 你可以通过结合旧数据和新数据,随着时间推移逐步完善该画像。

当使用新的 SPT 文件运行 SPDConvert 时,请传入现有 SPD 文件的名称。 然后,使用 /retire:N 选项来控制在添加新的 SPT 文件时,SPDConvert 在多大程度上降低对较旧配置文件数据的重视:

  • 默认值 (/retire:8) 对较新的数据进行更重的权重。
  • 使用 /retire:0 对所有运行赋予相同的权重。
  • 使用 /retire:16 以确保只有最新的数据才算数。

故障排除

查找问题:

LBR 路径问题

问题 可能的原因 修复
SPTAggregate 输出中的 LBR 样本数为零 CPU 不支持 LBR,或者 VM 不公开 LBR 检测您的路径中运行检测命令。 如果在 Hyper-V VM 中,请在主机上运行 Set-VMProcessor MyVMName -Perfmon @("pmu", "lbr")。 如果 LBR 不可用,请改用 PMC 或操作系统计时器路径。
处理器支持 LBR,但 SPTAggregate 显示 0 LBR 示例 perfcore.ini DLL 注册不完整 perfcore.ini“配置 perfcore.ini”中完成设置。 确保 perf_lbr.dll 已注册。
SPDConvert 失败或生成空 SPD 错误的 /mode 标志,或 SPT 仅包含 IP 模式示例 确认 SPTAggregate 输出显示了 LBR 示例。 如果输出仅显示 IP 模式示例,请切换到 /mode:IP

PMC路径问题

问题 可能的原因 修复
SPTAggregate 输出中没有 PMC 样本 perfcore.ini DLL 注册不正确 perfcore.ini“配置 perfcore.ini”中完成设置。 确保 perf_spt.dll 已注册。 如果没有该 DLL,xperf 会生成零个 PMC 采样结果,且不会显示错误消息。
运行 xperf.exe -pmcsources 以查看 CPU 上可用的性能计数器源列表。 如果您看不到类似 SPT_OP_RETIRE_INSTRSPT_OP_RETIRE_BR_INSTRSPT_OP_ETW_INSTR 的条目,则 perfcore.ini 中的 DLL 注册可能不完整,或者您的 CPU 可能不支持 PMC。 如果无法解决 DLL 注册问题,请改用操作系统计时器路径。
findstr InstructionRetired 返回输出,但 xperf 不产生任何样本 VM 屏蔽 PMC 计数器 检查是否在 VM 中运行。 使用 Set-VMProcessor 在 Hyper-V 中启用 PMU,或切换到 OS 计时器路径。
SPDConvert PMC 路径失败 在纯 IP 的 SPT 上使用 /mode:LBR 切换到 /mode:IP

OS 计时器路径问题

问题 可能的原因 修复
改进程度低于预期 预期情况 - OS 计时器精度较低 这是正常的。 优化器从定时器样本中获取的分支流信息少于从 LBR 或 PMC 中获取的分支流信息。 性能提升较小。 如果硬件支持 PMC 或 LBR,请考虑升级到 PMC 或 LBR。
零计时器示例 xperf 未在提升权限的命令提示符中运行,或缺少 PROFILE 提供程序 确认以管理员身份运行。 确认已向xperf命令提供-stackwalk profile

常见问题(所有路径)

问题 可能的原因 修复
"failed to configure counters" 错误 xperf 未以管理员身份运行 管理员 身份重启命令提示符(右键单击以 > 管理员身份运行)。 xperf 需要提升的权限才能配置硬件性能计数器。
xperf 找不到 xperf.exe 未在 PATH 中 确认已安装 Windows ADK。 请检查 C:\Program Files (x86)\Windows Kits\10\Windows Performance Toolkit\。 将该目录添加到 PATH,或直接从该路径运行 xperf。
textCount.etl 未创建 xperf 以无提示方式失败 确认以管理员身份运行。 重新运行 xperf start 命令并检查错误输出。
SPTAggregate 失败,并提示“找不到二进制文件” textCount.exe 不在当前目录中或错误路径中 确认你与 textCount.exe 位于同一目录中,或为 /binary 参数提供完整路径。
未创建 SPD 文件 SPDConvert 失败 textCount.spt检查大小是否为非零。 运行 SPTDump.exe textCount.spt 以检查其内容。
/spdin 构建没有带来任何改进 SPD 与二进制文件的 GUID/age 不匹配 SPD 是基于不同的 textCount.exe 构建的。 再次对当前构建版本进行性能分析,以生成新的 SPD。
/spgo 上的 MSVC 版本错误 早于 v14.51 的 MSVC 工具集 打开 Visual Studio Installer,在 >单个组件> 中安装 MSVC v14.51 或更高版本。 重新打开开发人员命令提示符。

后续步骤

完成本教程后,请浏览这些功能,从 SPGO 获取更多内容:

  • 配置文件合并:运行多个工作负载,收集每次运行生成的 SPT 文件,并将它们全部传递给 SPDConvert。 混合 SPD 能反映真实使用模式的全貌,并且比单一场景的配置文件带来更优的优化效果。 使用 /retire:N 选项可控制在添加新的 SPT 文件时,SPDConvert 降低对较旧配置文件数据重视程度的幅度。 默认值 (/retire:8) 对较新的数据进行更重的权重。 使用 /retire:0 可对所有运行赋予相同的权重;使用 /retire:16 则仅将最新数据计入。
  • 最佳结果来自融合多个来源的性能分析数据,例如针对关键场景进行压力测试的基准数据,以及真实世界数据(如果可用)。 将 SPT 文件从所有源传递到 SPDConvert. 在参数列表中重复某个 SPT 文件,以增加其权重(例如,SPDConvert myapp.spd critical.spt critical.spt common.sptcritical.spt 的加权是对 common.spt 的两倍)。
  • 迭代优化: 每次重新生成时 /spdin 都会生成一个新的 SPD。 你可以重复进行运行、性能分析和重新生成这一过程。 后续迭代的收益可能会递减,但第二遍处理有时能发现第一遍遗漏的模式。
  • 代码更改: 在源代码发生重大更改后,请重新收集性能分析数据。 现有的 SPD 与对其进行性能剖析时所针对的二进制文件绑定。 它将无法匹配经过大幅重建的二进制文件。
  • 性能分析数据新鲜度: 链接器会在每次 /spdin 构建后报告使用性能分析数据进行优化的已分析函数所占的百分比。 如果此百分比显著下降,则表明代码与配置文件存在分歧。 重新分析当前二进制文件的性能。