C++ Dynamic Debugging (Preview)

Important

C++ Dynamic Debugging is currently in preview. This information relates to a prerelease feature that might be substantially modified before release. Microsoft makes no warranties, expressed or implied, with respect to the information provided here.

This preview feature, available starting with Visual Studio 2022 Version 17.14 Preview 2, applies only to x64 projects.

With C++ Dynamic Debugging, you can debug optimized code as if it were unoptimized. This feature is useful for developers who require the performance benefits of optimized code, such as game developers who need high frame rates. With C++ Dynamic Debugging, you can enjoy the debugging experience of unoptimized code without sacrificing the performance advantages of optimized builds.

Debugging optimized code presents challenges. The compiler repositions and reorganizes instructions to optimize code. The result is more efficient code, but it means:

  • The optimizer can remove local variables or move them to locations unknown to the debugger.
  • Code inside a function might no longer align with source code when the optimizer merges blocks of code.
  • Function names for functions on the call stack might be wrong if the optimizer merges two functions.

In the past, developers dealt with these problems and others when they were in the process of debugging optimized code. Now these challenges are eliminated because with C++ Dynamic Debugging you can step into optimized code as if it were unoptimized.

In addition to generating the optimized binaries, compiling with /dynamicdeopt generates unoptimized binaries that are used during debugging. When you add a breakpoint, or step into a function (including __forceinline functions), the debugger loads the unoptimized binary. Then you can debug the unoptimized code for the function instead of the optimized code. You can debug as if you're debugging unoptimized code while you still get the performance advantages of optimized code in the rest of the program.

Try out C++ Dynamic Debugging

First, let's review what it's like to debug optimized code. Then you can see how C++ Dynamic Debugging simplifies the process.

  1. Create a new C++ console application project in Visual Studio. Replace the contents of the ConsoleApplication.cpp file with the following code:

    // 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. Change the Solution Configurations dropdown list to Release. Ensure that the solutions platform dropdown list is set to x64.

  3. Rebuild by selecting Build > Rebuild Solution.

  4. Set a breakpoint on line 55, int neighbors = countNeighbors(grid, i, j); in updateGrid(). Run the program.

  5. When you hit the breakpoint, view the Locals window. On the main menu, select Debug > Windows > Locals. Notice that you can't see the value of i or j in the Locals window. The compiler has optimized them away.

  6. Try to set a breakpoint on line 19, cout << (grid[i][j] ? '*' : ' '); in printGrid(). You can't. This behavior is expected because the compiler optimized the code.

Stop the program and enable C++ Dynamic Debugging and try it again

  1. In Solution Explorer, right-click the project and select Properties to open the project property pages.

  2. Select Advanced > Use C++ Dynamic Debugging, and change the setting to Yes.

    A screenshot that shows the advanced project properties.

    The property page opens to Configuration Properties > Advanced > Use C++ Dynamic Debugging. The property is set to Yes.

    This step adds the /dynamicdeopt switch to the compiler and the linker. Behind the scenes, it also turns off the C++ optimization switches /GL and /OPT:ICF. This setting doesn't overwrite switches that you added manually to the command line or other optimization switches that are set, such as /O1.

  3. Rebuild by selecting Build > Rebuild Solution. Build diagnostic code MSB8088 appears, which indicates that dynamic debugging and whole program optimization are incompatible. This error means that whole program optimization (/GL) was automatically turned off during compilation.

    You can manually turn off whole program optimization in the project properties. Select Configuration Properties > Advanced > Whole Program Optimization, and change the setting to Off. Now MSB8088 is treated as a warning, but it might be treated as an error in a future version of Visual Studio.

  4. Rerun the app.

    Now when you hit the breakpoint on line 55, you see the values of i and j in the Locals window. The Call Stack window shows that updateGrid() is deoptimized and the filename is life.alt.exe. This alternate binary is used to debug optimized code.

    A screenshot that shows debugging the updateGrid function.

    A breakpoint is shown in the function updateGrid. The call stack shows that the function is deoptimized and the filename is life.alt.exe. The Locals window shows the values of i and j and the other local variables in the function.

    The updateGrid() function is deoptimized on demand because you set a breakpoint in it. If you step over an optimized function while debugging, it isn't deoptimized. If you step into a function, it's deoptimized. The main way to cause a function to be deoptimized is if you set a breakpoint in it or step into it.

    You can also deoptimize a function in the Call Stack window. Right-click the function, or a selected group of functions, and select Deoptimize on next entry. This feature is useful when you want to view local variables in an optimized function for which you haven't set a breakpoint elsewhere on the call stack. Functions that are deoptimized in this way are grouped together in the Breakpoints window as a breakpoint group named Deoptimized Functions. If you delete the breakpoint group, the associated functions revert to their optimized state.

Use conditional and dependent breakpoints

  1. Try setting a breakpoint again on line 19, cout << (grid[i][j] ? '*' : ' '); in printGrid(). Now it works. Setting a breakpoint in the function deoptimizes it so that you can debug it normally.

  2. Right-click the breakpoint on line 19, select Conditions, and set the condition to i == 10 && j== 10. Then select the Only enable when the following breakpoint is hit: checkbox. Select the breakpoint on line 55 from the dropdown list. Now the breakpoint on line 19 doesn't hit until the breakpoint on line 50 is hit first, and then when grid[10][10] is about to output to the console.

    The point is that you can set conditional and dependent breakpoints in an optimized function and make use of local variables and lines of code that in an optimized build might be unavailable to the debugger.

    A screenshot that shows the conditional breakpoint settings for line 19.

    A conditional breakpoint is shown on line 19, cout < < (grid[i][j] ? '*' : ' ');. The condition is set to i == 10 && j== 10. The checkbox for Only enable when the following breakpoint is hit is selected. The breakpoint dropdown list is set to life.cpp line 55.

  3. Continue running the app. When the breakpoint on line 19 is hit, you can right-click line 15 and select Set Next Statement to rerun the loop again.

    A screenshot that shows debugging the printGrid function.

    A conditional and dependent breakpoint is hit on line 19, cout < < (grid[i][j] ? '*' : ' ');. The Locals window shows the values of i and j and the other local variables in the function. The Call Stack window shows that the function is deoptimized and the filename is life.alt.exe.

  4. Delete all the breakpoints to return deoptimized functions to their optimized state. On the Visual Studio main menu, select Debug > Delete All Breakpoints. All functions then return to their optimized state.

    If you add breakpoints via the Call Stack window Deoptimize on next entry option, which we didn't do in this walkthrough, you can right-click the Deoptimized Functions group and select Delete to revert only the functions in that group back to their optimized state.

    A screenshot that shows the Breakpoints window.

    The Breakpoints window shows the Deoptimized Functions group. The group is selected and the context menu is open with Delete Breakpoint Group selected.

Turn off C++ Dynamic Debugging

You might need to debug optimized code without it being deoptimized, or put a breakpoint in optimized code and have the code stay optimized when the breakpoint hits. There are several ways to turn off Dynamic Debugging or keep it from deoptimizing code when you hit a breakpoint:

  • On the Visual Studio main menu, select Tools > Options > Debugging > General. Clear the Automatically deoptimize debugged functions when possible (.NET 8+, C++ Dynamic Debugging) checkbox. The next time the debugger starts, code remains optimized.
  • Many dynamic debugging breakpoints are two breakpoints: one in the optimized binary and one in the unoptimized binary. In the Breakpoints window, select Show Columns > Function. Clear the breakpoint associated with the alt binary. The other breakpoint in the pair breaks in the optimized code.
  • When you're debugging, on the Visual Studio main menu, select Debug > Windows > Disassembly. Ensure that it has focus. When you step into a function via the Dissassembly window, the function won't be deoptimized.
  • Disable dynamic debugging entirely by not passing /dynamicdeopt to cl.exe, lib.exe, and link.exe. If you're consuming third-party libraries and can't rebuild them, don't pass /dynamicdeopt during the final link.exe to disable Dynamic Debugging for that binary.
  • To quickly disable Dynamic Debugging for a single binary (for example, test.dll), rename or delete the alt binary (for example, test.alt.dll).
  • To disable Dynamic Debugging for one or more .cpp files, don't pass /dynamicdeopt when you build them. The remainder of your project is built with Dynamic Debugging.

Enable C++ Dynamic Debugging in Unreal Engine

Unreal Engine 5.6 supports C++ Dynamic Debugging for both Unreal Build Tool and Unreal Build Accelerator. There are two ways to enable it:

  • Modify your project's Target.cs file to contain WindowsPlatform.bDynamicDebugging = true.

  • Use the Development Editor configuration, and modify BuildConfiguration.xml to include:

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

For Unreal Engine 5.5 or earlier, cherry-pick the Unreal Build Tool changes from GitHub into your repo. Then enable bDynamicDebugging as indicated above. You also need to use Unreal Build Accelerator from Unreal Engine 5.6. Either use the latest bits from ue5-main, or disable UBA by adding the following to BuildConfiguration.xml:

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

For more information about configuring how Unreal Engine is built, see Build Configuration.

Troubleshooting

If breakpoints don't hit in deoptimized functions:

  • If you step out of a [Deoptimized] frame, you might be in optimized code unless the caller was deoptimized due to a breakpoint in it or you stepped into the caller on your way to the current function.

  • Ensure that the alt.exe and alt.pdb files built. For test.exe and test.pdb, test.alt.exe and test.alt.pdb must exist in the same directory. Ensure that the right build switches are set according to this article.

  • A debug directory entry exists in test.exe that the debugger uses to find the alt binary to use for Deoptimized Debugging. Open an x64-native Visual Studio command prompt and run link /dump /headers <your executable.exe> to see if a deopt entry exists. A deopt entry appears in the Type column, as shown in the last line of this example:

      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
    

    If the deopt debug directory entry isn't there, confirm that you're passing /dynamicdeopt to cl.exe, lib.exe, and link.exe.

  • Dynamic Deoptimization won't work consistently if /dynamicdeopt isn't passed to cl.exe, lib.exe, and link.exe for all .cpp, .lib, and binary files. Confirm that the proper switches are set when you build your project.

For more information about known issues, see C++ Dynamic Debugging: Full Debuggability for Optimized Builds.

If things aren't working as expected, open a ticket at Developer Community. Include as much information as possible about the issue.

General notes

IncrediBuild 10.24 supports C++ Dynamic Debugging builds.

Functions that are inlined are deoptimized on demand. If you set a breakpoint on an inlined function, the debugger deoptimizes the function and its caller. The breakpoint hits where you expect it to, as if your program was built without compiler optimizations.

A function remains deoptimized even if you disable the breakpoints within it. You must remove the breakpoint for the function to revert to its optimized state.

Many dynamic debugging breakpoints are two breakpoints: one in the optimized binary and one in the unoptimized binary. For this reason, you see more than one breakpoint on the Breakpoints window.

The compiler flags that are used for the deoptimized version are the same as the flags that are used for the optimized version, except for optimization flags and /dynamicdeopt. This behavior means that any flags that you set to define macros, and so on, are set in the deoptimized version too.

Deoptimized code isn't the same as debug code. The deoptimized code is compiled with the same optimization flags as the optimized version, so asserts and other code that rely on debug-specific settings aren't included.

Build system integration

C++ Dynamic Debugging requires that compiler and linker flags must be set in a particular way. The following sections describe how to set up a dedicated configuration for Dynamic Debugging that doesn't have conflicting switches.

If your project is built with the Visual Studio build system, a good way to make a Dynamic Debugging configuration is to use Configuration Manager to clone your Release or Debug configuration and make changes to accommodate Dynamic Debugging. The following two sections describe the procedures.

Create a new Release configuration

  1. On the Visual Studio main menu, select Build > Configuration Manager to open Configuration Manager.

  2. Select the Configuration dropdown list and then select <New...>.

    A screenshot that shows Configuration Manager.

    In Configuration Manager, under Project contexts, the Configuration dropdown list is open and is highlighted.

  3. The New Solution Configuration dialog opens. In the Name field, enter a name for the new configuration, such as ReleaseDD. Ensure that Copy settings from: is set to Release. Then select OK to create the new configuration.

    A screenshot that shows the New Project Configuration dialog.

    The Name field is set to ReleaseDD. The Copy settings from: dropdown list is set to Release.

  4. The new configuration appears in the Active solution configuration dropdown list. Select Close.

  5. With the Configuration dropdown list set to ReleaseDD, right-click your project in Solution Explorer and select Properties.

  6. In Configuration Properties > Advanced, set Use C++ Dynamic Debugging to Yes.

  7. Ensure that Whole Program Optimization is set to No.

    A screenshot that shows the advanced project properties.

    The property page is opened to Configuration Properties > Advanced. Use C++ Dynamic Debugging. The property is set to Yes. Whole Program Optimization is set to No.

  8. In Configuration Properties > Linker > Optimization, ensure that Enable COMDAT folding is set to No (/OPT:NOICF).

    A screenshot that shows the Linker optimization project properties.

    The property page is opened to Configuration Properties > Linker > Optimization > Enable CMDAT Folding. The property is set to No (/OPT:NOICF).

This setting adds the /dynamicdeopt switch to the compiler and the linker. With C++ optimization switches /GL and /OPT:ICF also turned off, you can now build and run your project in the new configuration when you want an optimized release build that you can use with C++ Dynamic Debugging.

You can add other switches that you use with your retail builds to this configuration so that you always have exactly the switches turned on or off that you expect when you use Dynamic Debugging. For more information about switches that you shouldn't use with Dynamic Debugging, see Incompatible options.

For more information about configurations in Visual Studio, see Create and edit configurations.

Create a new Debug configuration

If you want to use debug binaries but you want them to run faster, you can modify your Debug configuration.

  1. On the Visual Studio main menu, select Build > Configuration Manager to open Configuration Manager.

  2. Select the Configuration dropdown list and then select <New...>.

    A screenshot that shows Configuration Manager.

    In Configuration Manager, in the Project contexts part of the window, the Configuration dropdown list is open and is highlighted.

  3. The New Project Configuration dialog opens. In the Name field, enter a name for the new configuration, such as DebugDD. Ensure that Copy settings from: is set to Debug. Then select OK to create the new configuration.

    A screenshot that shows the New Project Configuration dialog.

    The name field is set to DebugDD. The Copy settings from: dropdown list is set to Debug.

  4. The new configuration appears in the Active solution configuration dropdown list. Select Close.

  5. With the Configuration dropdown list set to DebugDD, right-click your project in Solution Explorer and select Properties.

  6. In Configuration Properties > C/C++ > Optimization, turn on the optimizations that you want. For example, you could set Optimization to Maximize Speed (/O2).

  7. In C/C++ > Code Generation, set Basic Runtime Checks to Default.

  8. In C/C++ > General, disable Support Just My Code Debugging.

  9. Set Debug Information Format to Program Database (/Zi).

You can add other switches that you use with your debug builds to this configuration so that you always have exactly the switches turned on or off that you expect when you use Dynamic Debugging. For more information about switches that you shouldn't use with Dynamic Debugging, see Incompatible options.

For more information about configurations in Visual Studio, see Create and edit configurations.

Custom build system considerations

If you have a custom build system, ensure that you:

  • Pass /dynamicdeopt to cl.exe, lib.exe, and link.exe.
  • Don't use /ZI, any of the /RTC flags, or /JMC.

For build distributors:

  • For a project named test, the compiler produces test.alt.obj, test.alt.exp, test.obj, and test.exp. The linker produces test.alt.exe, test.alt.pdb, test.exe, and test.pdb.
  • You need to deploy the new toolset binary c2dd.dll alongside c2.dll.

Incompatible options

Some compiler and linker options are incompatible with C++ Dynamic Debugging. If you turn on C++ Dynamic Debugging by using the Visual Studio project settings, incompatible options are automatically turned off unless you specifically set them in the additional command-line options setting.

The following compiler options are incompatible with C++ Dynamic Debugging:

/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

The following linker options are incompatible with C++ Dynamic Debugging:

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

See also

/dynamicdeopt compiler flag (preview)
/DYNAMICDEOPT linker flag (preview)
C++ Dynamic Debugging: Full Debuggability for Optimized Builds
Debug optimized code