C++动态调试(预览版)

重要

C++动态调试目前处于 预览版中。 此信息与可能在发布前进行实质性修改的预发行版功能有关。 Microsoft 不对此处提供的信息作任何明示或默示的担保。

从 Visual Studio 2022 版本 17.14 预览版 2 开始,此预览功能仅适用于 x64 项目。

使用C++动态调试,可以像未优化一样调试优化的代码。 此功能适用于需要优化代码的性能优势的开发人员,例如需要高帧速率的游戏开发人员。 使用C++动态调试,你可以享受未优化代码的调试体验,而无需牺牲优化生成的性能优势。

调试优化代码会带来挑战。 编译器重新定位并重新组织指令以优化代码。 结果是更高效的代码,但这意味着:

  • 优化器可以删除局部变量,或将其移动到调试器未知的位置。
  • 优化器合并代码块时,函数内的代码可能不再与源代码保持一致。
  • 如果优化器合并两个函数,则调用堆栈上的函数名称可能不正确。

过去,开发人员在调试优化代码的过程中处理这些问题和其他问题。 现在,这些挑战已经被消除,因为使用 C++ 动态调试功能,你可以像代码未经过优化一样,单步执行已经过优化的代码。

除了生成优化的二进制文件之外,使用 /dynamicdeopt 进行编译还会生成调试期间使用的未优化二进制文件。 添加断点或单步执行函数(包括 __forceinline 函数)时,调试程序回加载未优化的二进制文件。 然后,可以调试函数的未优化代码,而不是优化代码。 可以像调试未优化的代码一样进行调试,同时仍可在程序其余部分获得优化代码的性能优势。

试用C++动态调试

首先,让我们回顾一下调试经过优化代码的体验。 然后,可以看到C++动态调试如何简化该过程。

  1. 在 Visual Studio 中创建新的C++控制台应用程序项目。 将 ConsoleApplication.cpp 文件的内容替换为以下代码:

    // Code generated by GitHub Copilot
    #include <iostream>
    #include <chrono>
    #include <thread>
    
    using namespace std;
    
    int step = 0;
    const int rows = 20;
    const int cols = 40;
    
    void printGrid(int grid[rows][cols])
    {
        cout << "Step: " << step << endl;
        for (int i = 0; i < rows; ++i)
        {
            for (int j = 0; j < cols; ++j)
            {
                cout << (grid[i][j] ? '*' : ' ');
            }
            cout << endl;
        }
    }
    
    int countNeighbors(int grid[rows][cols], int x, int y)
    {
        int count = 0;
        for (int i = -1; i <= 1; ++i)
        {
            for (int j = -1; j <= 1; ++j)
            {
                if (i == 0 && j == 0)
                {
                    continue;
                }
    
                int ni = x + i;
                int nj = y + j;
                if (ni >= 0 && ni < rows && nj >= 0 && nj < cols)
                {
                    count += grid[ni][nj];
                }
            }
        }
        return count;
    }
    
    void updateGrid(int grid[rows][cols])
    {
        int newGrid[rows][cols] = { 0 };
        for (int i = 0; i < rows; ++i)
        {
            for (int j = 0; j < cols; ++j)
            {
                int neighbors = countNeighbors(grid, i, j);
                if (grid[i][j] == 1)
                {
                    newGrid[i][j] = (neighbors < 2 || neighbors > 3) ? 0 : 1;
                }
                else
                {
                    newGrid[i][j] = (neighbors == 3) ? 1 : 0;
                }
            }
        }
        // Copy newGrid back to grid
        for (int i = 0; i < rows; ++i)
        {
            for (int j = 0; j < cols; ++j)
            {
                grid[i][j] = newGrid[i][j];
            }
        }
    }
    
    int main()
    {
        int grid[rows][cols] = { 0 };
    
        // Initial configuration (a simple glider)
        grid[1][2] = 1;
        grid[2][3] = 1;
        grid[3][1] = 1;
        grid[3][2] = 1;
        grid[3][3] = 1;
    
        while (true)
        {
            printGrid(grid);
            updateGrid(grid);
            std::this_thread::sleep_for(std::chrono::milliseconds(100));
            cout << "\033[H\033[J"; // Clear the screen
            step++;
        }
    
        return 0;
    }
    
  2. 将“解决方案配置”下拉列表更改为“发布”。 确保解决方案平台下拉列表设置为 x64

  3. 通过选择“生成”“重新生成解决方案”来重新生成。>

  4. int neighbors = countNeighbors(grid, i, j); 的第 55 行 updateGrid() 处设置断点。 运行程序。

  5. 命中断点时,查看“局部变量”窗口。 在主菜单上,选择“调试”“Windows”>“局部变量”。> 请注意,在 i 窗口中看不到 j 的值。 编译器已将它们优化掉。

  6. 尝试在 cout << (grid[i][j] ? '*' : ' '); 中的第 19 行 printGrid() 处设置断点。 你不能。 此行为是预期的,因为编译器优化了代码。

停止程序并启用C++动态调试,然后重试

  1. 解决方案资源管理器中,右键单击项目并选择 属性 打开项目属性页。

  2. 选择“高级”使用 C++ 动态调试”,并将设置更改为“是”。>

    显示高级项目属性的屏幕截图。

    属性页将打开转到“配置属性”>“高级”>“使用 C++ 动态调试”。 该属性设置为“是”。

    此步骤将 /dynamicdeopt 开关添加到编译器和链接器。 在后台,它还会关闭 C++ 优化开关 /GL/OPT:ICF。 此设置不会覆盖你手动添加到命令行的开关或其他已设置的优化开关,例如 /O1

  3. 通过选择“生成”“重新生成解决方案”来重新生成。> 此时会显示生成诊断代码 MSB8088,指示动态调试和整个程序优化不兼容。 此错误表示在编译期间自动关闭整个程序优化(/GL)。

    可以在项目属性中手动关闭整个程序优化。 选择 配置属性>高级>整个程序优化,并将设置更改为 关闭。 现在,MSB8088 被视为警告,但在 Visual Studio 的未来版本中,它可能被视为错误。

  4. 重新运行应用。

    现在,在第 55 行命中断点时,可在“局部变量”窗口中看到 ij 的值。 调用堆栈 窗口显示 updateGrid() 被取消优化,文件名是 life.alt.exe。 此备用二进制文件用于调试优化的代码。

    显示调试 updateGrid 函数的屏幕截图。

    updateGrid 函数中显示了断点。 调用堆栈显示函数已取消优化,文件名为 life.alt.exe。 “局部变量”窗口显示函数中的 i 和 j 和其他局部变量的值。

    updateGrid() 函数按需去优化,因为你在其中设置了断点。 如果在调试过程中越过优化函数,该函数仍保持优化状态。 如果单步执行某个函数,该函数会被去优化。 去优化函数的主要方法是在函数中设置断点或单步执行该函数。

    还可以在 调用堆栈 窗口中取消优化函数。 右键单击该函数或所选的函数组,然后在下一个条目 中选择“取消优化”。 如果要在优化函数中查看局部变量,而你尚未在调用堆栈的其他位置设置断点,此功能非常有用。 以这种方式被去优化的函数在 断点 窗口中作为 去优化函数的断点组被归类到一起。 如果删除断点组,则关联的函数将还原为其优化状态。

使用条件断点和依赖断点

  1. 尝试在 cout << (grid[i][j] ? '*' : ' '); 中的第 19 行 printGrid() 处设置断点。 现在它起作用了。 在函数中设置断点会对该函数去优化,以便你可正常调试它。

  2. 右键单击第 19 行的断点,选择 条件,并将条件设置为 i == 10 && j== 10。 然后,选择“仅在命中以下断点时启用:”复选框。 从下拉列表中选择第 55 行上的断点。 现在,不会命中第 19 行的断点,直到先命中第 50 行的断点,然后 grid[10][10] 即将输出到控制台时。

    关键是可以在优化后的函数中设置条件断点和依赖断点,并利用在优化构建中可能对调试器不可用的局部变量和代码行。

    显示第 19 行的条件断点设置的屏幕截图。

    条件断点显示在第 19 行 cout < < (grid[i][j] ? '*' : ' ');。 条件设置为 i == 10 && j== 10。 已选中“仅在命中以下断点时启用”复选框。 断点下拉列表设置为 life.cpp 第 55 行。

  3. 继续运行应用。 当第 19 行的断点被命中时,可以右键单击第 15 行,然后选择“设置下一个语句”,再次运行循环。

    显示调试 printGrid 函数的屏幕截图。

    条件断点和依赖断点在第 19 行 cout < < (grid[i][j] ? '*' : ' '); 被命中。 “局部变量”窗口显示函数中的 i 和 j 和其他局部变量的值。 “调用堆栈”窗口显示该函数已被取消优化,文件名为 life.alt.exe。

  4. 删除所有断点,将去优化函数恢复到优化状态。 在 Visual Studio 主菜单上,选择 调试>删除所有断点。 然后,所有函数返回其优化状态。

    如果通过“调用堆栈”窗口的“下次进入时去优化”选项添加断点(本演练未涉及此操作),可以右键单击“去优化的函数”组,然后选择“删除”,仅将该组中的函数恢复至优化状态。

    显示“断点”窗口的屏幕截图。

    “断点”窗口显示“去优化的函数”组。 该组被选中,上下文菜单被打开,并选择了“删除断点组”。

关闭C++动态调试

可能需要调试已优化的代码,而不对其去优化,或者在已优化的代码中设置断点,同时在断点命中后让代码保持在优化状态。 在命中断点时,可通过多种方式关闭动态调试或防止其去优化代码:

  • 在 Visual Studio 主菜单上,选择 工具>选项>调试>常规。 请取消选中“在可能时自动去优化已调试的函数(.NET 8+,C++ 动态调试)”复选框。 下次调试器启动时,代码将保持优化。
  • 许多动态调试断点分为两个:一个在优化的二进制文件中,另一个在未优化的二进制文件中。 在“断点”窗口中,选择“显示列”“函数”。> 清除与 alt 二进制文件关联的断点。 对中的其他断点在优化的代码中发生中断。
  • 调试时,在 Visual Studio 主菜单上,选择“调试”“Windows”>“反汇编”。> 确保它具有焦点。 当你通过“反汇编”窗口单步执行函数时,该函数不会被去优化。
  • 不将 /dynamicdeopt 传递给 cl.exelib.exelink.exe,从而完全禁用动态调试。 如果你正在使用第三方库且无法重建它们,请不要在最终 /dynamicdeopt 期间传递 link.exe 来禁用该二进制文件的动态调试。
  • 若要快速禁用单个二进制文件的动态调试(例如,test.dll),请重命名或删除 alt 二进制文件(例如,test.alt.dll)。
  • 若要为一个或多个 .cpp 文件禁用动态调试,请在生成文件时不传递 /dynamicdeopt。 项目的其余部分是使用动态调试生成的。

在 Unreal Engine 中启用C++动态调试

Unreal Engine 5.6 支持针对 Unreal 生成工具和 Unreal 生成加速器C++动态调试。 可通过两种方法启用它:

  • 修改项目的 Target.cs 文件以包含 WindowsPlatform.bDynamicDebugging = true

  • 使用 开发编辑器 配置,并修改 BuildConfiguration.xml 以包括:

    <WindowsPlatform>
        <bDynamicDebugging>true</bDynamicDebugging>
    </WindowsPlatform>
    

对于 Unreal Engine 5.5 或更低版本,请从 GitHub 中选择 Unreal Build Tool 更改并放入你的存储库中。 然后,按上面所示启用 bDynamicDebugging。 你还需要使用来自 Unreal Engine 5.6 的 Unreal 构建加速器。 使用 ue5-main 的最新位,或者通过将以下内容添加到 BuildConfiguration.xml 来禁用 UBA:

<BuildConfiguration>
    <bAllowUBAExecutor>false</bAllowUBAExecutor>
    <bAllowUBALocalExecutor>false</bAllowUBALocalExecutor>
</BuildConfiguration>

有关配置 Unreal Engine 生成方式的详细信息,请参阅 生成配置

故障排除

如果断点未在去优化的函数中命中:

  • 如果单步跳出 [Deoptimized] 帧,则你可能进入优化代码,除非调用方因为其中的断点而被去优化,或者你在到达当前函数的过程中已单步执行调用方。

  • 确保已构建 alt.exealt.pdb 文件。 对于 test.exetest.pdbtest.alt.exetest.alt.pdb 必须存在于同一目录中。 确保根据本文设置正确的构建选项。

  • debug directory 中存在一个 test.exe 条目,调试程序使用该条目查找用于去优化调试的 alt 二进制文件。 打开 x64 本机 Visual Studio 命令提示符并运行 link /dump /headers <your executable.exe> 以查看是否存在 deopt 条目。 deopt 条目显示在 Type 列中,如本示例中的最后一行所示。

      Debug Directories
    
            Time Type        Size      RVA  Pointer
        -------- ------- -------- -------- --------
        67CF0DA2 cv            30 00076330    75330    Format: RSDS, {7290497A-E223-4DF6-9D61-2D7F2C9F54A0}, 58, D:\work\shadow\test.pdb
        67CF0DA2 feat          14 00076360    75360    Counts: Pre-VC++ 11.00=0, C/C++=205, /GS=205, /sdl=0, guardN=204
        67CF0DA2 coffgrp      36C 00076374    75374
        67CF0DA2 deopt         22 00076708    75708    Timestamp: 0x67cf0da2, size: 532480, name: test.alt.exe
    

    如果 deopt 调试目录条目不存在,请确认将 /dynamicdeopt 传递给 cl.exelib.exelink.exe

  • 如果 /dynamicdeopt 未传递至 所有 cl.exelib.exe 和二进制文件的 link.exe.cpp.lib,动态去优化可能无法一致地工作。 确认在生成项目时设置了正确的开关。

有关已知问题的详细信息,请参阅 C++动态调试:优化生成的完整调试性

如果未按预期工作,请在开发人员社区提交工单。 尽可能多地包含有关该问题的信息。

一般说明

IncrediBuild 10.24 支持C++动态调试版本。

内联的函数根据需要进行去优化。 如果在内联函数上设置断点,调试器将使函数与其调用方失去优化状态。 断点在你预期的位置命中,就像你的程序在没有编译程序优化的情况下构建一样。

即使禁用了其中的断点,函数仍保持不优化。 必须删除函数的断点才能恢复到其优化状态。

许多动态调试断点分为两个:一个在优化的二进制文件中,另一个在未优化的二进制文件中。 因此,你会在“断点”窗口中看到多个断点。

用于未优化版本的编译器标志与用于优化版本的标志相同,但优化标志和 /dynamicdeopt除外。 此行为意味着还会在去优化的版本中设置你用于定义宏的任何标志,等等。

取消优化代码与调试代码不同。 非优化代码使用与优化版本相同的优化标志进行编译,因此不包含依赖于调试特定设置的断言和其他代码。

构建系统集成

C++动态调试要求必须以特定方式设置编译器和链接器标志。 以下部分介绍如何为没有冲突开关的动态调试设置专用配置。

如果项目是使用 Visual Studio 生成系统生成的,则进行动态调试配置的好方法是使用 Configuration Manager 克隆发布或调试配置,并进行更改以适应动态调试。 以下两节介绍了这些过程。

创建新的发布配置

  1. 在 Visual Studio 主菜单上,选择 生成>Configuration Manager 打开 Configuration Manager。

  2. 选择 配置 下拉列表,然后选择“新建...”<>

    显示 Configuration Manager 的屏幕截图。

    在 Configuration Manager 的“项目”上下文下,“配置”下拉列表处于打开状态,并突出显示

  3. 此时会打开 “新建解决方案配置” 对话框。 在 名称 字段中,输入新配置的名称,例如 ReleaseDD。 确保从“从此处复制设置:”设置为“发布”。 然后选择 “确定” 以创建新配置。

    显示“新建项目配置”对话框的屏幕截图。

    “名称”字段设置为 ReleaseDD。 “从此处复制设置:”下拉列表设置为“发布”。

  4. 新配置显示在 活动解决方案配置 下拉列表中。 选择 关闭

  5. 配置 下拉列表设置为“ReleaseDD”后,右键单击解决方案资源管理器 中的项目,然后选择 属性

  6. 在“配置属性”“高级”中,将“使用 C++ 动态调试”设置为“是”。>

  7. 确保“整个程序优化”设置为“否”。

    显示高级项目属性的屏幕截图。

    属性页将打开转到“配置属性”>“高级”。 使用C++动态调试。 该属性设置为“是”。 整个程序优化设置为“否”。

  8. 在“配置属性”“链接器”>“优化”中,确保将 “启用 COMDAT 折叠”设置为“否(/OPT:NOICF)”。>

    显示链接器优化项目属性的屏幕截图。

    属性页将打开转到“配置属性”>“链接器”>“优化”>“启用 CMDAT 折叠”。 该属性设置为“否”(/OPT:NOICF)。

此设置将 /dynamicdeopt 开关添加到编译器和链接器。 在关闭C++优化开关 /GL/OPT:ICF 后,你现在可以在新的配置中构建和运行项目,以便在需要一个可以用于C++动态调试的优化发布版本时使用。

您可以将与零售版本一起使用的其他开关添加到此配置中,以确保在使用动态调试时,开关的开启或关闭状态完全符合您的预期。 若要详细了解不应该用于动态调试的开关,请参阅不兼容的选项

有关 Visual Studio 中的配置的详细信息,请参阅 创建和编辑配置。

创建新的调试配置

如果要使用调试二进制文件,但希望它们运行得更快,可以修改调试配置。

  1. 在 Visual Studio 主菜单上,选择 生成>Configuration Manager 打开 Configuration Manager。

  2. 选择 配置 下拉列表,然后选择“新建...”<>

    显示 Configuration Manager 的屏幕截图。

    在 Configuration Manager 中,在窗口的项目上下文部分中,“配置”下拉列表处于打开状态,并突出显示

  3. 新建项目配置”对话框打开。 在 名称 字段中,输入新配置的名称,例如 DebugDD。 确保从“从此处复制设置:”设置为“调试”。 然后选择 “确定” 以创建新配置。

    显示“新建项目配置”对话框的屏幕截图。

    名称字段设置为 DebugDD。 “从此处复制设置:”下拉列表设置为“调试”。

  4. 新配置显示在 活动解决方案配置 下拉列表中。 选择 关闭

  5. 配置 下拉列表设置为 DebugDD,右键单击解决方案资源管理器 中的项目,然后选择 属性

  6. 配置属性>C/C++>优化中,打开所需的优化。 例如,可以将 优化 设置为 最大化速度(/O2)

  7. C/C++>代码生成中,将 基本运行时检查 设置为 默认

  8. 在“C/C++”“常规”中,禁用“支持仅我的代码调试”。>

  9. 调试信息格式 设置为 Program Database (/Zi)

可以将与调试构建一起使用的其他开关添加到此配置中,以便在使用动态调试功能时,始终能够精确地打开或关闭你期望的开关。 若要详细了解不应该用于动态调试的开关,请参阅不兼容的选项

有关 Visual Studio 中的配置的详细信息,请参阅 创建和编辑配置。

自定义构建系统注意事项

如果有自定义生成系统,请确保:

  • /dynamicdeopt 传递给 cl.exelib.exelink.exe
  • 请勿使用 /ZI、任何 /RTC 标志或 /JMC

对于生成分发服务器:

  • 对于名为 test的项目,编译器生成 test.alt.objtest.alt.exptest.objtest.exp。 链接器生成 test.alt.exetest.alt.pdbtest.exetest.pdb
  • 需要部署新的工具集二进制文件 c2dd.dllc2.dll

不兼容的选项

某些编译器和链接器选项与C++动态调试不兼容。 如果使用 Visual Studio 项目设置打开C++动态调试,则除非你在其他命令行选项设置中专门设置它们,否则将自动关闭不兼容的选项。

以下编译器选项与C++动态调试不兼容:

/GH
/GL
/Gh
/RTC1 
/RTCc 
/RTCs 
/RTCu 
/ZI (/Zi is OK)
/ZW 
/clr 
/clr:initialAppDomain
/clr:netcore
/clr:newSyntax
/clr:noAssembly
/clr:pure
/clr:safe
/fastcap
/fsanitize=address
/fsanitize=kernel-address

以下链接器选项与C++动态调试不兼容:

/DEBUG:FASTLINK
/INCREMENTAL
/OPT:ICF  You can specify /OPT:ICF but the debugging experience may be poor

另请参阅

/dynamicdeopt 编译器标志(预览版)
/DYNAMICDEOPT 链接器标志(预览版)
C++动态调试:优化生成的完整可调试性
调试优化代码