Printf Debugging Is OK
Alex Dixon highlights the programming community's debate on IDEs versus text editors, noting junior developers' lack of debugging experience. He advocates for various debugging tools and methods to improve skills.
Read original articleAlex Dixon discusses the ongoing debate in the programming community regarding the use of Integrated Development Environments (IDEs) and debuggers versus simpler text editors like Notepad for coding. He notes that while he regularly uses debuggers such as Visual Studio and Xcode, many junior developers lack experience with debugging tools, which are often not taught in academic settings. Dixon emphasizes that debugging is essential, especially in complex projects where bugs may arise from legacy code or contributions from others. He argues that tools like Address Sanitizer and Undefined Behavior Sanitizer can significantly aid in identifying issues that would otherwise be difficult to track down. While he primarily relies on debuggers, he acknowledges that there are scenarios where 'printf' debugging is necessary, particularly in release builds or when dealing with hardware events. He also advocates for the use of custom debugging tools that complement existing ones, enhancing the debugging process. Ultimately, Dixon concludes that the goal is to effectively find and fix bugs, regardless of the tools used, and encourages developers to explore various debugging methods to improve their skills.
- The debate over using IDEs versus simple text editors for coding continues in the programming community.
- Many junior developers lack debugging experience, which is often not covered in educational programs.
- Tools like Address Sanitizer can help identify bugs that are hard to track down in complex codebases.
- 'Printf' debugging can be useful in specific scenarios, such as release builds or hardware interactions.
- Custom debugging tools can enhance the debugging process and should complement existing tools.
Related
Features I'd like to see in future IDEs
Proposed improvements for IDEs include queryable expressions for debugging, coloration of comments, embedding images, a personal code vault for snippets, and commit masks to prevent accidental code commits.
Thoughts on Debugging
The article emphasizes the importance of reproducing issues in debugging, effective communication, logging for real-time analysis, clear documentation for recognition, and warns junior engineers about the absence of favors in business.
Kernighan's Lever (2012)
Kernighan's lever highlights that while clever coding can complicate debugging, facing challenges fosters skill development, making debugging a valuable learning opportunity that enhances programming capabilities.
Don’t look down on print debugging
Print debugging is a straightforward and effective method for identifying code issues, involving placing print statements, analyzing output, and removing them after resolution, while automated tests enhance the debugging process.
Mastering Ruby Debugging: From Puts to Professional Tools
Ruby developers must master debugging to fix code issues effectively. The article discusses tools ranging from simple puts statements to advanced debuggers, emphasizing the importance of understanding operator precedence.
- Many commenters agree that printf debugging is a valid and often effective method, especially in situations where setting up a debugger is cumbersome.
- Some emphasize the importance of understanding both debugging techniques, suggesting that each has its place depending on the context and complexity of the issue.
- There are concerns about the potential pitfalls of relying too heavily on printf debugging, such as the risk of leaving debug statements in production code.
- Several participants highlight the need for efficient debugging tools and practices, advocating for a balance between using print statements and more sophisticated debugging methods.
- Overall, the conversation reflects a broader debate about the best practices in programming and debugging, with a call for flexibility and adaptability in tool usage.
This sounds so dumb but it works out to be equivalent to some very powerful debugger features. You don't need a magical debugger that lets you modify code on-the-fly while continuing the same debug session... just change the code and re-run the test. You don't need a magical record-replay debugger that lets you go "back in time"... just add a printf earlier in the control flow and re-run the test. You don't need a magical debugger that can break when a property is modified... just temporarily modify the property to have a setter function and printf in the setter.
Most importantly, though, this sort of debugging is performed using the same language and user interface I use all day to write code in the first place, so I don't have to spend time trying to remember how to do stuff in a debugger... it's just code.
BUT... this is all contingent on having fast-running automated tests that can reproduce your bugs. But you should have that anyway.
The main symptom was a non-deterministic crash in the middle of a 15-minute multi-threaded execution that should have been 100% deterministic. The debugger revealed that the contents of an array had been modified incorrectly, but stepping through the code prevented the crash, and it was not always the same array or the same position within that array. I suspected that the array writes were somehow dependent on a race, but placing a data breakpoint prevented the crash. So, I started dumping trace information. It was a rather silly game of adding more traces, running the 15-minute process 10 times to see if the overhead of producing the traces made the race disappear, and trying again.
The root cause was a "read, decompress and return a copy of data X from disk" method which was called with the 2023 assumption that a fresh copy would be returned every time, but was written with the 2018 optimization that if two threads asked for the same data "at the same time", the same copy could be returned to both to save on decompression time...
Anecdote aside, it certainly doesn't hurt to be able to debug things without a debugger if it comes to that.
Also, will we ever move forward from these sort of discussions? Back when I was a mechanic no one argued about basics troubleshooting strategies. We just aimed to learn them and apply them all (as necessary).
Long story short, our game worked as long as the printfs we had were kept, we had macro to remove them (in "Release/Ship") but the game crashed.
The crash was due to side-effect of printf clearing some math errors.... So here you go!
one thing I think the "just do print debugging" folks miss is what a good teaching tool a visual debugger is: you can learn about what a call stack really is, step through conditionals, iterations, closures, etc and get a feel for how they really work
at some level being a good programmer means you can emulate the code in your head, and a good visual developer can really help new programmers develop that skill, especially if they aren't naturals
i emphasize the debugger in all the classes i teach for this reason
If you don't know what a debugger does though that's something you should really get on ASAP. Likewise if you can't figure out how to get log messages out of your thing. Really all there is to it, figure out what you want to do after than and spend your time actually doing something productive instead of getting in a stupid holy war on the internet about it.
printf'ing effectively enforces a similar condition to `volatile` on the underlying memory segment when it is read.
One can encounter tersely written code that works perfectly with printf statements, but status bits never get "updated" (CPU cache purged) without the printf and hangs the program.
I work in a natural science, and use computing for numerical simulations and data analyses. For coding problems, a debugger is pretty handy. But for finding errors in the underlying mathematics, I tend to rely on a printf -> grep -> analysis chain. This might make files of several hundred Mb in size. The last part is generally done in R, because that lets me apply graphical and statistical methods to discover problematic conditions. Often these conditions do not crop up for quite a long time, and my task will be to find out just what I did wrong, with something like a problematic formulation of boundary condition that created an anomalous signal near an edge of a domain, but only once a signal had propagated from a forcing zone into that region of state space.
printf debugging is a symptom of poor tooling. It is like saying that driving in nails with a rock is fine. It works, but the truth is that if you are using a rock, that's probably because you don't have a good hammer. And if on every job site, there are seasoned workers banging rocks like cavemen, maybe hammer manufacturers should take notice.
Honestly I think attaching a debugger should be the first debugging tool you reach for. Sometimes printf debugging is required. Sometimes printf debugging is even better. But a debugger should always be the tool of first choice.
And if your setup makes it difficult to attach a debugger then you should re-evaluate your setup. I definitely jump through hoops to make sure that all the components can be run locally so I can debug. Sure you’ll have some “only in production” type bugs. But the more bugs you can trivially debug locally the better.
Of course I also primarily write C++ code. If you’re stuck with JavaScript you maybe the calculus is different. I wouldn’t know.
Sometimes a series of print-statements are better for helping you understand exactly when and how a bug occurs. This is particularly true in situations where the debugger is difficult or impossible to use (e.g., multi-threading). Of course, in that situation, logging may be better, but that's just glorified printf.
How are those cases debugged then? By enabling the debug symbols AND the optimizations and using the debugger, looking at the code and the disassembly side by side and trying to keep your sanity as the steps hop back and forth through the code. Telling yourself that the bug is real and it just cannot be reproduced easily because it depends on multiple factors + hardware states. Ah! I sometimes miss those kinds of bugs which make you question your reality.
> I know for some people this is often a terrible UX because of the performance of debug builds, so a prerequisite here is fast debug builds.
The reasons debug builds perform badly are kind of mixed, in my experience looking at other people's set ups:
Building without optimisations
It's fairly common to believe that debug builds have to be built with -O0 (no optimisations) but this isn't true (at least, not on the most common platforms out there). There's no need to build something that's too slow to be useful.
You can always add debug info by using -g (on gcc / clang). Use -g3 to get the maximum level. This is independent of optimisation.
You can build with any level of optimisation you want and the debugger will do its best to provide you a sensible interpretation - at higher optimisation levels this can give some unintuitive behaviours but, fundamentally, the debugger will still work.
Gcc provides the "-Og" optimisation level, which attempts to balance reasonably intuitive behaviour under the debugger with decent performance (clang also supports this but, last I checked, it's just an alias to -O1.
Doing a ton of self-checks
People often add a load of self-checking, stress testing behaviours and other things "I might want when looking for a bug" to their code and gate it on the NDEBUG macro.
The logic here is reasonable - you have a build that people use for debugging, so over time that build accumulates loads of behaviours that might help find a bug.
The trouble is, this can give you a build that's too slow / weird in its behaviours to actually be representative. And then it's no use for finding some of your bugs anymore!
I think it would be better here to have a separate "dimension" for self-checking (e.g. have a separate macro you define to activate it), rather than forcing "debug build" to mean so many things.
In VS Code, if you want to run debugger with arguments (especially for CLI programs), you have to put these arguments in launch.json and then run the debugger.
This is often tedious to do, because I usually have typed these arguments and tested in terminal before, and now I have to convert them into json format, which is annoying.
To make it worse, VS Code uses a separate window for debug console than your main terminal, so they don't share history/output.
So if I know what to look at already and don't really need a full debugger, I often just use print() temporally.
In theory, you can make conditional breakpoints very fast using an in-process agent. For GDB (for instance) this gives the ability to evaluate conditional breakpoints within the process itself, rather than switching back to GDB: https://sourceware.org/gdb/current/onlinedocs/gdb.html/In_00...
I've always found the GDB documentation to be a bit vague about how you set up the in-process agent but I found this: (see 20.3.4 "Tracepoints support in gdbserver") https://sourceware.org/gdb/current/onlinedocs/gdb.html/Serve...
When we implemented in-process evaluation of conditional breakpoints in UDB (https://undo.io/products/udb/) it made the software run about 3000x faster than bare GDB with a frequently-hit conditional breakpoint in place. In principle, with a setup that just uses GDB and in-process evaluation you should do even better.
Are you diligent enough to remove your sensitive logging/printf statements EVERY time, for the rest of your career? Or should you make a habit of doing things properly from day one?
If we have no other option then sometimes we have to use non-ideal approaches, but I don't get the impulse to start saying that tooling/observability poverty is actually good.
That said, there are some contexts where this is reversed - printing something useful without affecting the debugged code may actually be more involved than, say, attaching a JTAG probe and stepping through with a debugger. Though sometimes both of those are a luxury you can't afford, so you better be able to manage without them anyway (and this may happen to you regardless of whether you're working on low-level hardware bring-up or some shiny new JavaScript framework).
Everytime you have to do a printf it's a slowdown, and you can't out-argue the fact that you have to type up to 20ish keystrokes and excite a number of brain neuron cells trying to remember what that printf syntax or variable name was. In comparison to a debugger that automatically prints out your call stack and local variables even without you having to prompt them.
The key insight is that printf() is a heavyweight operation ("What, you want to build a string? A human readable string? Okay, one second, lemme just pull in the locale library..."). If you're debugging something at the business-logic layer, it's probably fine.
If you're debugging a memory leak, calling a function that's going to make a deep-dive on the stack and move a lot of memory around is likely to obscure your bug.
My print statements normally come when "I have no idea why this is breaking", and I start sanity checking the basics to make sure that things I previously thought were true, remain so.
Just recently I was doing something in C after a long time, and had something like this (simplified):
#include <stdio.h>
int main(){
int a = 0; // Input from elsewhere
switch(a){
case 0 :
printf("0\n");
break;
defult :
printf("?\n");
break;
}
return 0;
}
It was late at night, it compiled, so I knew it wasn't a grammar issue. But after testing with printf()'s I realised that the default case was never being hit and performing the core action. It turns out 'defult' highlights in my editor and compiles fine in GCC. Turns out that any word in that location compiles fine in GCC. Nasty!Other than that, people should spend time learning the ins and outs of their debugging tools, like they do for the compiler switches and language details.
Additionally, when having the option to pick the programming language, it isn't only the syntax sugar of the grammar, or its semantics that matter, it is the whole ecosystem, including debugging tools.
Personally I rather have a great debugging experience than less characters per line of code.
That said, I was pleasantly surprised I was able to attach a debugger to that system. Some bugs really needed it.
[1]: https://andydote.co.uk/2024/11/24/print-debugging-tracing/
sadly there is no standard way to do this (c++ is reportedly getting one in '26: https://en.cppreference.com/w/cpp/utility/breakpoint), so you just need to use what your platform provides. here's a partial list: https://stackoverflow.com/a/49079078
e.g. __debugbreak in msvc, asm("int3") on x86[_64], raise(SIGTRAP) in posix.
Sure you can printf or run gdb, or whathever, but first if something like a contract has failed it will be easyer.
Stepping through code is more like having your nose on the ground.
Both have their merits.
My holier-than-thou take on this topic is: Whenever possible, debug by adding assert statements and test cases.
The only time it is not OK is when breakpoint debugging is overall faster but you are avoiding the hassle of setting up the debugger.
Also OK: adding a console.log or print in your node modules or python package cache.
And btw splunk, datadog etc. is just printf at scale.
Been waiting for "something else" ~30 years & counting.
Me too!
I know I'm stirring up shit here but there really are benefits to touch typing (I mean just think about it, using 10 fingers instead of 2 is gonna be so much faster assuming you have 10 dingers)
I use vim, IDEs, debuggers, printf debugging, whatever works. A tool is a tool. I guess my holier-than-thou position is against the idea that there's one right way to do anything.
Logging is great for long term issue resolution. There's tracepoints/logpoints which let you refine the debugging experience in runtime without accidentally committing prints to the repo.
There are specific types of projects that are very hard to debug (I'm working on one right now), that's a valid exception but it also indicates something that should be fixed in our stack. Print debugging is a hack, do we use it? Sure. Is it OK to hack? Maybe. Should a hack be normalized? Nope.
Print debugging is a crutch that covers up bad logging or problematic debugging infrastructure. If you reach for it too much it probably means you have a problem.
Related
Features I'd like to see in future IDEs
Proposed improvements for IDEs include queryable expressions for debugging, coloration of comments, embedding images, a personal code vault for snippets, and commit masks to prevent accidental code commits.
Thoughts on Debugging
The article emphasizes the importance of reproducing issues in debugging, effective communication, logging for real-time analysis, clear documentation for recognition, and warns junior engineers about the absence of favors in business.
Kernighan's Lever (2012)
Kernighan's lever highlights that while clever coding can complicate debugging, facing challenges fosters skill development, making debugging a valuable learning opportunity that enhances programming capabilities.
Don’t look down on print debugging
Print debugging is a straightforward and effective method for identifying code issues, involving placing print statements, analyzing output, and removing them after resolution, while automated tests enhance the debugging process.
Mastering Ruby Debugging: From Puts to Professional Tools
Ruby developers must master debugging to fix code issues effectively. The article discusses tools ranging from simple puts statements to advanced debuggers, emphasizing the importance of understanding operator precedence.