Debugging in Release

It can be very tricky to debug problems using an optimized build, primarily because of the way the compiler optimizes the code. Ideally, every programmer would prefer to debug in a debug build, but this is often not possible.

These non-debug-only bugs are sometimes caused by uninitialized variables, because variables and dynamically allocated memory blocks are often set to zero in debug mode but are left containing garbage in a non-debug build. Another common reason for non-debug-only bugs includes code that has been accidentally omitted from the non-debug builds. For example, it is when important code is erroneously placed inside an assertion statement. Other common causes include data structures whose size or data member packing changes between debug and release builds, bugs that are only triggered by inlining or compiler-introduced optimizations, and bugs in the compiler’s optimizer itself.

The best ways to reduce the pain of debugging optimized code is to practice doing it. Here are a few tips.

Learn to read and step through disassembly in the debugger.

In a non-debug build, the debugger often has trouble keeping track of which line of source code is currently being executed. Thanks to instruction reordering, the program counter jumps around erratically within the function when viewed in source code mode. However, things become sane again when working with the code in disassembly mode.

Use registers to deduce variables’ value or addresses.

The debugger will sometimes be unable to display the value of a variable or the contents of an object in a non-debug build. However, if the program counter is not too far away from the initial use of the variable, there’s a good chance its address or value is still stored in one of the CPU’s registers. If tracing back through the disassembly to where the variable is first loaded into a register, its value or its address can be often discovered by inspecting that register. Use the register window, or type the name of the register into a watch window, to see its contents.

Inspect variables and object contents by address.

Given the address of a variable or data structure, its contents can be obtained by casting the address to the appropriate type in a watch window. For example, if an instance of the Foo class resides at address 0x1378A0C0, type (Foo*)0x1378A0C0 in a watch window, and the debugger will interpret that memory address as if it were a pointer to a Foo object.

Leverage static and global variables.

Even in an optimized build, the debugger can usually inspect global and static variables. If it seems not possible to deduce the address of a variable or object, find a static or global variable that might contain its address, either directly or indirectly.

Modify the code.

If it is relatively easy to reproduce a non-debug-only bug, consider modifying the source code. Add print statements to see what is going on. Introduce a global variable to make it easier to inspect a problematic variable or object in the debugger. Add code to detect a problem condition or to isolate a particular instance of a class.

Reference

[1] J. Gregory, Game Engine Architecture, Third Edition, CRC Press


© 2025. All rights reserved.