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.
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; }
Change the Solution Configurations dropdown list to Release. Ensure that the solutions platform dropdown list is set to x64.
Rebuild by selecting Build > Rebuild Solution.
Set a breakpoint on line 55,
int neighbors = countNeighbors(grid, i, j);
inupdateGrid()
. Run the program.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
orj
in the Locals window. The compiler has optimized them away.Try to set a breakpoint on line 19,
cout << (grid[i][j] ? '*' : ' ');
inprintGrid()
. 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
In Solution Explorer, right-click the project and select Properties to open the project property pages.
Select Advanced > Use C++ Dynamic Debugging, and change the setting to Yes.
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
.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.Rerun the app.
Now when you hit the breakpoint on line 55, you see the values of
i
andj
in the Locals window. The Call Stack window shows thatupdateGrid()
is deoptimized and the filename islife.alt.exe
. This alternate binary is used to debug optimized code.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
Try setting a breakpoint again on line 19,
cout << (grid[i][j] ? '*' : ' ');
inprintGrid()
. Now it works. Setting a breakpoint in the function deoptimizes it so that you can debug it normally.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 whengrid[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 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.
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 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.
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.
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
tocl.exe
,lib.exe
, andlink.exe
. If you're consuming third-party libraries and can't rebuild them, don't pass/dynamicdeopt
during the finallink.exe
to disable Dynamic Debugging for that binary. - To quickly disable Dynamic Debugging for a single binary (for example,
test.dll
), rename or delete thealt
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 containWindowsPlatform.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
andalt.pdb
files built. Fortest.exe
andtest.pdb
,test.alt.exe
andtest.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 intest.exe
that the debugger uses to find thealt
binary to use for Deoptimized Debugging. Open an x64-native Visual Studio command prompt and runlink /dump /headers <your executable.exe>
to see if adeopt
entry exists. Adeopt
entry appears in theType
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
tocl.exe
,lib.exe
, andlink.exe
.Dynamic Deoptimization won't work consistently if
/dynamicdeopt
isn't passed tocl.exe
,lib.exe
, andlink.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
On the Visual Studio main menu, select Build > Configuration Manager to open Configuration Manager.
Select the Configuration dropdown list and then select <New...>.
In Configuration Manager, under Project contexts, the Configuration dropdown list is open and
is highlighted. 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.The Name field is set to ReleaseDD. The Copy settings from: dropdown list is set to Release.
The new configuration appears in the Active solution configuration dropdown list. Select Close.
With the Configuration dropdown list set to ReleaseDD, right-click your project in Solution Explorer and select Properties.
In Configuration Properties > Advanced, set Use C++ Dynamic Debugging to Yes.
Ensure that Whole Program Optimization is set to No.
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.
In Configuration Properties > Linker > Optimization, ensure that Enable COMDAT folding is set to No (/OPT:NOICF).
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.
On the Visual Studio main menu, select Build > Configuration Manager to open Configuration Manager.
Select the Configuration dropdown list and then select <New...>.
In Configuration Manager, in the Project contexts part of the window, the Configuration dropdown list is open and
is highlighted. 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.
The name field is set to DebugDD. The Copy settings from: dropdown list is set to Debug.
The new configuration appears in the Active solution configuration dropdown list. Select Close.
With the Configuration dropdown list set to DebugDD, right-click your project in Solution Explorer and select Properties.
In Configuration Properties > C/C++ > Optimization, turn on the optimizations that you want. For example, you could set Optimization to Maximize Speed (/O2).
In C/C++ > Code Generation, set Basic Runtime Checks to Default.
In C/C++ > General, disable Support Just My Code Debugging.
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:
For build distributors:
- For a project named
test
, the compiler producestest.alt.obj
,test.alt.exp
,test.obj
, andtest.exp
. The linker producestest.alt.exe
,test.alt.pdb
,test.exe
, andtest.pdb
. - You need to deploy the new toolset binary
c2dd.dll
alongsidec2.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