Coding To Debug
By Kyle Wilson
Monday, December 09, 2002
A fellow named Michael Pohoreski recently asked on SWEng-GameDev what separates people who are good at debugging code from people who are bad at debugging code. Here's my response, annotated for the web.
I think good debugging skills, while important, are less important than writing code which can easily be debugged. Programmers are optimists, and even those of us with enough experience that we ought to know better tend to skip the dull work required to make code solid and maintainable. So we spend lots of time fixing at the end of the process problems that could have been fixed cheaply if caught early.
My advice for making code easy to debug:
Test everything you can in the compiler. Don't use ints or enums for flags -- instead, make lightweight wrapper classes so you reap the benefits of type-safety. (Like my own FlagSet class. I hate that Win32 has a million types, all of which are an unsigned int to the compiler.) Use compile-time asserts where possible.
Use runtime asserts where reasonable. Testing class invariants every time you enter and exit a class method, Eiffel-style, might be a bit too much, but you should at least assert that parameters passed to functions are valid. Asserting that array accesses are in range is a good idea too. But you should be using vectors instead of arrays anyway, right?
Draw a sharp line between data and code. Asserts are for testing code correctness, not for verifying data. Incorrect data should be detected on load and the user should be notified. Invalid data should never be passed on into the code, where it'll start setting off asserts and triggering crashes or undefined behavior.
Write standalone tests and run them with your daily build.
Make your data structures easy to navigate in the debugger. Give game objects string identifiers that programmers can peek into and numeric identifiers that breakpoints can break on. Prefer vectors or arrays to linked lists (it's easier to view their contents in a variable window). Prefer smart pointers to handles (they're much easier to indirect through). If you're writing your own memory allocators, follow the example of Microsoft's C runtime library and put in an automatic conditional breakpoint like _crtBreakAlloc. Breakpoints in a debugger take much, much longer to check.
Speed up the debugging cycle. Debugging tends to involve a lot of overhead in incremental changes, recompiles and relinks. Avoid recompiling files you don't have to by not inlining functions unless profiling says you should. Use precompiled headers. Avoid cyclic dependencies in your code.
Shorten the change/recompile cycle. If you make it easier for programmers to gather the data they need in-game, they won't need to recompile so much. Put a console in the game. Let programmers display and alter object properties from the console. Make performance information easily accessible. Implement alternate display modes (depth mode, show normals, etc.) to make it easier to track down graphical oddities. Implement frame-based recording and playback so you can consistently recreate any bug.
For useful tips on debugging with Visual Studio, check http://www.highprogrammer.com/alan/windev/visualstudio.html.
I'm Kyle Wilson. I've worked in the game industry since I got out of grad school in 1997. Any opinions expressed herein are in no way representative of those of my employers.