The unreasonable effectiveness of print debugging
buttondown.email
buttondown.email
> As personal choice, we tend not to use debuggers beyond getting a stack trace or the value of a variable or two. One reason is that it is easy to get lost in details of complicated data structures and control flow; we find stepping through a program less productive than thinking harder and adding output statements and self-checking code at critical places. Clicking over statements takes longer than scanning the output of judiciously-placed displays. It takes less time to decide where to put print statements than to single-step to the critical section of code, even assuming we know where that is. More important, debugging statements stay with the program; debugging sessions are transient.
I found this lines up with my personal experience. I used to lean on interactive debuggers a lot, and still enjoy using them. They're fun and make for good exploring. But the act of figuring out where you want to print really makes you think in ways that interactive debugging cannot. I find the two forms really complement each other.
Circa 1999, one would assume that Brian Kernighan and Rob Pike are largely drawing experience from working with C, which is a relatively verbose language. Single stepping through C code in a debugger is indeed a laborious process.
If you read accounts from Smalltalk developers, on the other hand, it's clear that they very nearly live their entire lives inside the debugger, and consider it to be an enormous productivity booster.
I would guess that there are several effects going on there. One would be that, in terms of how much effective work it accomplishes, a line of Smalltalk code is generally not equivalent to a line of C code. That has a big impact on just how many steps are involved in single-stepping your way through a region. The other is the nature of the debugger itself. Gdb and Smalltalk debuggers are wildly different pieces of software.
The core of his position (as I understand it) was that regularly needing a debugger is a sign that your software has "gotten away from you". You've let the software get to a state where it cannot easily be understood from the architecture and program text alone.
I do think debuggers can be useful when building up comprehension - particularly of other people's software.
It's very time consuming though, and at each "trace" you're only seeing one path through the program. Good architecture and documentation lets you understand all the possible paths at once.
If it's being used too often then it points to the fact that the software has to be run in order to understand it. It's representation is not sufficient to convey it's run time behaviour.
Also would like to point that dynamic languages in general require more debugging than statically typed one's since one can't be certain of the data flow within functions.
In such a case it's difficult to decide what to print or more particularly how to print it if it's type/shape is unknown.
In those situations, it's often just so much easier to set a breakpoint and take a peek than it is to waste brain cycles on thinking about what exactly you should even be printing in the first place.
We need more tooling to help people understand and mitigate necessary complexity, not tools that help one muddle through or — I shudder to think — extend complexity.
I’ve changed my mind on this recently only because some IDEs have indeed become good at the latter.
Rather they do live changes of their code, and then evaluate various objects to verify that things work as expected. That how I seem to remember working in Smalltalk many years ago.
I am not a fan of debuggers, although I do use the REPL a lot. I suppose like Rob Pike, I used them only for very limited tasks, such as getting a stack trace or getting some sense of control flow. But as soon as I have that, I spend more time looking at code and reasoning about it, than stepping in a debugger. With a REPL I can try out assumptions I make about the code, rather than being forced to step through it.
You can also make a weird sort of UI this way, where the way you interact with your program is by changing the code / and or state while it's running. Breakpoints prompt for input.
Add to this a need to build a debugging-enabled version of the project - an often long-running process, compared to a few edits to some 'release' build.
On the other hand, when dealing with an unfamiliar or complex and well-forgotten project, debuggers become that discovery and exploration tool that offers a wider context and potentially better situational awareness.
Of course, mix-in some concurrency and either debugging approach can equally become cumbersome without proper understanding of the project.
Uh, me too. That's why I don't single-step through huge chunks of a program.
I use code breakpoints.
Using the programming language itself, I can extract exactly the information I need to see, transform it into exactly the shape that's easiest for me to inspect and combine output from different places in the code to create a compact list of state changes that's easy for my eyes to scan.
What I have found is that my debugging problems are either too simple to require anything more than thinking or too data dependent for a debugger to be the best tool for the job.
Rather then stepping through a program. Add breakpoints and just step from breakpoint to breakpoint.
With a good IDE, adding a breakpoint and hitting a shortcut key is faster than a print statement and on the GUI IDE your debugging sessions are not transient.
The only time their advice makes sense to me is when I'm in an environment without a gui. Even then jetbrains has a "ssh full remote mode." However at my company I have found that this feature doesn't work under Nix (nix-shell) so we all just use print statements.
In C++ I often find the debugger doesn't find symbols on projects built with configure/Make. If I have a Java Gradle project I have no idea how to get it into a debugger. Python debuggers always seem fragile. Rust requires I install and use a "rust-gdb" script -- except on my current machine that doesn't work and I don't know why.
I'm sure in each case I'm sure I could get a debugger working given enough time, but the only error I've ever had in print debugging is failing to flush output before a crash, and it's never been hard to search for "how to flush output" in whatever language I'm current using.
Yes I am dieing on this hill.
Debuggers aren't bad. But neither is printing. Knowing when to reach for them is probably a bit more key.
Same applies to other debuggers.
It is not only the debuggers, but also OS and language runtime tracing facilities like DTrace, eBPF, ETW, JFR, ....
Many devs aren't 10x because of QI, rather because they learn and make use of the tools available to increase knowledge about the platform.
So we get generations that use vim and Emacs like notepad, create Makefiles by copy-paste and barely know gdb beyond set breakpoint, run, step and continue.
Using C as example, but feel free to extrapolate to another language.
And no I am not exaggerating, this was the kind of students I would get into my lab when I spent a year as TA in 1999/2000, and naturally would have to get them up to speed into good programming practices.
The worst developers I've ever known always have their "this is the best way to do everything" hill they die on.
The problem is that the two print statements will only catch the bug if they are the right two, based on a correct hypothesis of what the bug is. Which, with a debugger, won’t require stepping, but setting two breakpoints, doing a run-to-breakpoint, and inspecting values.
Stepping is required when you are exploring behavior because you don’t have an easily testable hypothesis about the source of the bug.
As you note, debugger breakpoints aren't magically better than print statements when I'm investigating a hypothesis – I'm no more likely to put them the right place than I would have put print statements.
And then there's a class of problems that neither debugger nor print statements will help: many years ago a very junior co-worker was wondering why his C code was giving the wrong answer for some math. It took me pointing out that one of the numeric types he was using in his code was different from the rest (I think it was #defined elsewhere, in some library, as an integer type). When the compiler did the math it had to do some type coercing.
A debugger and watches on the values of concern absolutely will help with that (so will properly placed print statements), so its a really bad example. (Of course, strong typing helps even more with that particular case.)
Can't I also bisect with breakpoints?
I'd throw your comment back -- maybe all languages should be "debug first", make it as easy to get code into a debugger as it is to just build and run it.
It is an exaggeration. In practice, it is useful to apply both. Novices can get some insight using debugging, more experienced with code base people should exercise their understanding of the code and use well picked prints.
Also, I think what you suggest to do here is way harder to learn than printing.
Try debugging something in the embedded world, and you’ll see why a lot of bare metal programmers use printfs. Turns out timing is critical most of the time, so using a debugger hides a LOT of bugs from your eyes.
Debuggers are very useful, but so are prints, there just different tools, they have different purposes.
Adding a 'breakpoint()' at the start of the program does get me into the debugger. I'll remember that for future (but, it's not easy to find by googling if you don't already know what you are looking for!)
For large unwieldy data-structures, you can go ipython: `import IPython; IPython.embed()` launches the ipython REPL from the calling line's context.
I use the latter a fair bit when spelunking around in other people's code. `pdb.set_trace()` lets you continue execution more easily.
We haven't had one hit production yet, but it came close. Print statement is a lot more harmless.
Personally, I think folks should master the debugger _first_ and during all steps learning a programming language.
But similar to test-driven-development it’s a different way of thinking, and most books scarcely discuss the debuggers.
That being said, I do use print-debugging a lot too—in C++ a lot of functionality can be compiled-out, allowing one to, for instance, print hex dumps of serialized data going to the network.
On that note, there is a distinction between trace debugging that is part of the source code and general print statements that are hacked in and removed.
Also, pycharm isn't really what I would call a proper debugger yet, attaching remote running processes for example just doesn't work reliably yet and is very new anyways. Debugging embedded targets just doesn't work. Multithreading is iffy (but that's unfortunately normal in Python).
On the other hand... adding print statements can also invalidate certain optimizations (an excellent source of heisenbugs), so I'll never stop using debuggers either
Setting up remote debugging I’ll agree is more difficult than a local application, but each remote machine can automatically run startup commands and not require user input; commands can be run at particular places too (to print output etc) with conditional trace points, all while not impacting the code itself.
Main point is that folks don’t spend enough time learning the debugger, as print statements are easier. But using the debugger is a better practice in my opinion in the case where print statements are added just for a quick test, then removed.
Having said that, it is absolutely a requirement when working on a project for any length of time (especially professionally) to set up and figure out a debugging environment, because it is significantly more productive than printing. But the startup cost is certainly there.
In the Java case, for stanalone projects (i.e. not something deployed on a server) an if it is your own project and you don't do anything unreasonable it is mostly just set a breakpoint and hit "run with debugging".
Probably the least painful debugging experience I know.
Doing it for Tomcat/Tomee was slightly more advanced IMO but still utterly trivial compared to wrangling css or js ;-)
There are reasons why we "old folks" like Java so much despite its verboseness.
Being able to debug third party code in remote / hostile environments (even when its mixed with proprietary vendor code) is one of the things I like about Java.
``` import pdb; pdb.set_trace() ```
that's literally all you have to do at any point in your code. Run your code in the foreground of a terminal, and boom you have a debugger exactly where you want it.
You can edit the state and keep running after altering the program.
But usually I just end up printing a few things from the repl and figuring out what is fouled up.
You download Intellij IDEA, run it, choose File->Open and select the build.gradle file, right click the main class and there's a Debug option.
Geoff’s not wrong in invoking Bret Victor’s Learnable Programming argument that being able to track state over time is critical to debugging, and Geoff’s right that print debugging makes this easier than almost any existing step-debugger.
Bret’s deeper point, though, is that a major challenge in debugging is hidden state in general, and that variable state changing over time is just one example.
Not only is there a ton of hidden state in the execution of a program—the full execution trace PLUS state of every variable at each point in that trace—but there is also a ton of interpretation of that state that the programmer needs to do while debugging: “what does this sequence of events imply?” - “why is this pointer pointing here?” - etc.
Doing that interpretation is much easier when the programmer gets to selectively view that (again, HUGE amount of) hidden state. Print debugging gives the programmer complete control over what state is shown. No other debugger does that: they all show a ton of data and context (often useful!) and make certain operations easy (inspecting single variables! viewing call stack snapshot!), and these are often just the right things.
But sometimes they’re not. And often, when you start debugging, you don’t know if the fancy debuggers will be too much or not enough.
Print debugging gives you the power to write code to selectively view the (again, HUGE!) hidden state of your program, and this scales from the smallest code-tracing bug to the largest distributed systems.
Step debuggers, on the other hand, are essentially “no-code” debuggers — extremely useful for the purpose for which they are designed, still useful for adjacent purposes, and a great place to start if you know the tool well, but ultimately not as powerful if your needs exceed their capacities.
A good programmer will know how to use all these tools for what they’re best at!
Same thing with watch windows, memory views and the like. There are classes of problems that do well with printf but calling them "no-code" is vastly underselling them.
A data breakpoint is a great example of something useful that print doesn’t do well.
I also don't think they're nearly as no-code as you call out. VS' watch window has very few limitations compared to printf back when I was working on win32 things.
Also important to consider iteration time. I once worked on a system where adding a printf was a 20 minute process due to the need to heavily optimize for a memory constrained platform(scripting fit in 400kb block with the asset bake step).
Exactly!
Debuggers are very useful tools, and typically not as general-purpose as print. I don’t view “not as powerful” as a meaningful distinction, because it requires that you ask “powerful at what?” ---
VS’ watch window is great but (I assume) doesn’t work across distributed systems, etc. — as a general technique, print is universal in the sense that there are very few problems that can’t be diagnosed by modifying your code and printing some (possibly a manipulation!) of the hidden state. This is going to be harder than using a special-purpose tool designed for exactly your problem.
In the same way, “no-code” tools are typically better and/or easier than writing code to solve the same problem, but special-purpose.
In my domain which doesn't usually cover distributed systems printf can be worse because it introduces synchronization primitives that have caused race conditions to disappear(and that race condition causes second order heap corruption or the like). On one platform system memory was so small(8mb total) that each output to stdout went over the serial link slowing performance down to 1/20th of a realtime process under any real logging.
Like I said, different tools for different uses, and really depends on the context. If there was one size fits all then we'd just use that but the diversity of debugging tools I think shows that you need a variety of techniques to approach the problems we encounter.
We totally agree, and I'm not sure what we're arguing about--perhaps you can fill me in.
I'm arguing that print is almost always worse than any specialized tool. (After all, who would use a specialized tool worse than print?) There is not a one-size-fits-all tool, and print is not a one-size-fits-all tool.
Indeed almost every seasoned developer has a story about print failing. Whether it's the mysterious "Heisenbug" that disappears when you measure it (like the sync issues you mention) -- my personal story is when I was trying to debug a (class project) kernel scheduler. Printing to the console was so slow that by the time I'd printed anything, the scheduler had moved on to the next time slice!
It's worth nothing that "print debugging" is not literally just using the "print" function; it's a style of debugging that involves logging specific information using some logging function (usually, but not always, print) and then analyzing it after the fact (usually, but not always, by reading the printed output).
This strategy of "get data out, then analyze it" is the general form of print debugging, and in the small-memory case, or the sync Heisenbug case, this often means collecting data in the appropriate variables before outputting it to be visible. Isn't this still print debugging, even though it doesn't use a "print" function?
With print debugging your inserting the whole build + deploy + repro setup loop into your debugging, if that's a long time(say 20 minutes in one job I had with production hardware) you're in for a world of pain. I find that just about any other tool usually is an order of magnitude more efficient.
Also even the "step debugger" tools do the same thing you'd do with a print. LLVM for instance uses the IR JIT API to generate watch/eval values: https://releases.llvm.org/9.0.0/docs/ORCv2.html#use-cases
IMO you should relentlessly optimize your iteration times, that's the inner loop of development speed and print debugging fares pretty poorly in that area for all the reasons above.
Ah, that's fair.
> At least for me print debugging is a measure of last resort
Right, and I think this depends on the domain. For lots of mature environments, this makes sense -- there's been years for tooling to catch up to the kinds of bugs people run into, possibly corporate money being put into developing debugging tools, etc.
> IMO you should relentlessly optimize your iteration times, that's the inner loop of development speed and[...]
Agreed, though the effect on print debugging on iteration time is very environment-dependent.
> [...]print debugging fares pretty poorly in that area for all the reasons above
Adding console.log to a web app can be a trivial change (though of course reproducing app state is another issue) -- again very environment-dependent.
Debugging via print allows you to step outside and peer in ie it is active.
Print debugging can be prone to bugs within itself which may cause additional ignorance about the potential bugs being diagnosed. How meta can you get? 8) There's also the effect of the effort of actually looking - that may or may not have an effect.
Anyway, the discussion here is largely ignorant of language and function. At the moment I spend time fiddling up Python and OpenSCAD scripts if I dig out a programming language. For me, print is really handy. For a Linux low level latency sensitive driver in highly optimised ASM n C I suspect this matter is moot.
I'm not making the argument that people shouldn't use debuggers -- obviously if there's a good one that does what you need, it'd be silly not to use it. And good debuggers are great.
But what happens when you're working on a distributed system? Or multiple processes on a single system? Or...etc. etc. -- debuggers are almost always built to support a particular set of use cases for a particular set of domains. Step outside, and they're not usually as useful.
print always works. It's almost never the best. It's more flexible because it can almost always be used.
If you have some completely undocumented object oriented code with tons of relationships, peeling the state apart in a debugger is a good way to get an idea of what you even have that might be worth printing.
Option1: Print all of them, requires rebuild, would log 200000 lines every second. Unless i wrap the print inside conditions, requiring yet another rebuild.
Option2: Conditional breakpoint: user[i].department.balance <= expected_value. Bang, i can now inspect both the complete nested user-object, and the previous/next item in the list, other local and global variables, the call stack of how it reached there, the state of all other threads in this moment, and so on. With the really good debuggers like .NET or JVM I'm even able to rewind the execution pointer to the start of the function or hot-swap the code as it's running.
In short, a debugger allows you to see all state of the program at once, and you can retroactivelly choose what is relevant. As opposed to printf where you must select upfront. Maybe even more importantly it's lacking stack traces, unless you add a log and request-id at every function-enter/exit.
There is also the case of third party code which you can't edit to add printf to. Many environments usually by default give you pretty good context of the symbol names or even complete source code which you can step through (except c++ land where you'd need a debug-build of the lib, which isn't impossible either).
Lots of things but an obvious example is private member info.
I’ve been trying to articulate this to myself for a long time. Thank you.
That seems to be the idea behind Glamorous Toolkit https://gtoolkit.com/
I'd be curious what it would take to add any of those concepts to existing debuggers for other languages.
Nope, print debugging does not always work. Please stop saying that. Sure, it's often useful and often works. But go try to debug a race condition, or bug in some locking mechanism with print statements. I'll wait as you add a bunch of print statements, and then learn the harsh lesson that you are now debugging a completely different program with very different performance characteristics that now doesn't deadlock where it used to deadlock because you slowed down one of the threads massively as it spends time it used to not spend logging stuff.
"Step debuggers, on the other hand, are essentially “no-code” debuggers — extremely useful for the purpose for which they are designed, still useful for adjacent purposes, and a great place to start if you know the tool well, but ultimately not as powerful if your needs exceed their capacities."
I guess you'd think this if you thought print debugging "always works". But I promise you it is equally (if not easier) to exceed the capabilities of print statements in some scenarios.
Yeah, I run through valgrind; 9 times out of 10 I don't even get to the point of starting the debugger or recompiling with prints. The remaining 1 time where valgrind output is clean, I go with printing. About half the time the prints aren't enough and then I fire up the debugger.
Also, it depends. I'm working on a legacy C# project where the previous dev read somewhere that passwords shouldn't be stored in plain-text, so when changing the stored DB credentials you have to set a breakpoint where the hash is calculated, change the variable holding the cleartext password to the new password you want to use, step the line that calculates the new hash, copy the text out of the watch window and finally paste that text into the file holding the credentials.
I do not know how this helps. I also don't care enough to write a c/line utility that generates the hash in a base-64 string, becasue we've changed the DB credentials for the webapp only once since he left. We may have changed it more often if we had an easy way to do so :-/
And then you have to figure out the syntax... does it want package names, class names, or (hello Intellij plugins!) need a fucking # at the beginning to be recognized.
And then you have stuff like a "helpful" IDE that by default only shows WARN and above levels without telling you somewhere "there might be stuff you don't see" like Chrome does.
For actual debuggers, shit is worse, across the board. Running in Docker is always a recipe for issues, not to mention many applications actively messing around with stuff like ports.
A system.out.println always ends up somewhere sane.
These systems likely already have a way to get logs, but good luck getting a debugger to work there.
I agree.
A similar statement is that using a debugger often does not work.
the sort of ratholes I've run into are: - debug build - proper symbols - interrupts and debugger - kernel and debugger - unfamiliarity with debugger - limitations of debugger
Without a proper debug build, you can't run the debugger effectively. You have to set up a whole debug environment.
Sometimes you need to do broad work to get proper symbols and stack trace information. By broad, I mean a debug build for everything.
I've also found many debug builds, apart from altering some behavior, they also turn on a lot of printfs.
If you're using interrupts/timers or debugging a kernel module, many times debuggers don't work or may alter, move or supress the problem.
and then there's the... I haven't used a debugger in 6 months, how do I do (very simple thing). And sometimes the debugger is just not the right tool or a tedious tool to use. "If I just load this one macro and somehow get the right address maybe I can decode this one kernel data structure..."
Personally, many times an ephereral printf isn't crude and meaningless, it's a precise scalpel getting to the core of the problem.
FWIW my experience debugging Python in VS Code required no setup and I've encountered zero issues.
But also I want to nitpick because the title is one of my “favorite” pet peeves: “The Unreasonable Effectiveness of ...” thing is now used (as in this article) by people who are trying to say that something is remarkably or surprisingly effective, but that’s not what the original essay was about at all!
“The unreasonable effectiveness of the mathematics in the natural sciences” was a philosophy of science piece whose thesis was that there is no reasonable (rational, provable) basis for the degree to which our math abstractions and syllogisms happen to correspond to the physical universe.
It is self evident that they do in fact correspond super well, but the original piece was about how weird and spooky that actually is, if you think about it at all. Math is super effective, and there is no reasonable basis that we yet know that it should be so effective. It’s unreasonably effective.
It’s such a perfect title for that piece, and it feels dirty or diluting when it’s just used to mean “remarkably effective.”
The fluid and consistent (java is only a portion of what I write at work) editing experience is mostly more valuable to me than the slightly better autocompletion.
I keep intellij installed, but it only open it if I want to do a fancy mechanical refactor, like extract an interface from an existing class. Smaller niceties like creating a local variable from an expression are only a handful of keystrokes just feel like naturally describing what I want (lexically, rather than semantically, I'll admit) in vim anyway.
The hard part is, unlike a screwdriver where you can demonstrate, editors and "IT" in general are mental tools where the mindset is an invisible, nontransferable "handle" to the visible portion that everyone can see and use.
I would not hire a carpenter who doesn’t believe in using hammers. Neither would I constantly bug a hired carpenter to use the hammer I think they should be using instead of the one they like.
(map! :n "ff" #'save-buffer) ; Save
(map! :n "fq" #'kill-current-buffer) ; Quit a buffer
or (defun my/org-buffer-check ()
"Check that we are in org-directory, and the buffer name is in that directory"
(and (string= org-directory default-directory)
(seq-contains (directory-files org-directory nil)
(buffer-name)
'string=)))
the latter being easily represented as: function my/org-buffer-check()
return string=(org-directory, default-directory)
and seq-contains(directory-files(org-directory, nil),
buffer-name(),
&string=)
end
(seq-contains looks through the sequence returned by directory-files, for the result of buffer-name(), and compares them using the string equality function)I love the concept behind Emacs, I just think at least 80% of its code should actually be in plugins, and the program itself and a lot of large expansions are really bogged down by the sheer size and lack of simplicity.
Oh, and Emacs-Lisp...it's much better than Vimscript, but it's a disappointment nonetheless. Loops instead of recursion in Lisp, really? And last time I tried it the parser could not handle unmatched brackets in comments.
That's pretty common in common lisp as well. Specifically do loops (and lest we not forget the loop macro).
I think you might be thinking of the scheme branch of lisps, but not all of them work that way.
I switched last July and after 8+ years of using variations of Vim, Vi, Ex-Vi, Ed(1) (yes), NeoVim, et all, it is by far the smoothest experience I've ever had.
Unlike my experience with Spacemacs, I haven't had any problems adapting from Vim -- there are no points where the Vim interaction layer breaks down, and it genuinely feels like an editor that I'll be using for the next 20+ years. Like something that can grow around me.
Tabs vs spaces
This __wouldn't matter__ if we all just used TABS for indent level and if spaces were ignored for that: I also prefer tabs to show in a GUI code editor at ~4 characters, but be equivalent to 8 display characters in terminal modes (I guess EM size, but when I care about those I really want a 'fixed width' font, so toss all of the complexity aside please).
Multi people teams (or just one person editing on different platforms depending on need) using different editors and with different preferences would like to have a word with you here.
You can step through the program, reason about what's going on, tracking values as they change. But if you missed the moment, you have start again from the beginning (time traveling debuggers being rare). Or maybe you're looking at the wrong part entirely at this stage, and just wasting time.
With print debugging you write a bit of code to test a hypothesis. Then you run it, and you keep running it, and especially if it's an UI program you play with the UI and see how the values change during that run. Ideally the loop to change the code -> see the result should be a few seconds.
You can then git commit or stash your prints, switch branches and compare behavior with the same changes applied. And at the end of the day if you walk away, your prints will still be there the next morning. The debugger doesn't produce any comparable tangible artifacts.
Once you do know where the problem is, and if it's not apparent what the problem is (most problems are pretty trivial once located), that's IMO the time to break out the debugger and slowly step through it. But the vast majority of problems are faster to solve through rapid iterative exploration with prints in my experience (C, C++ for over a decade, Python, now JS/TS).
That's especially true if you're doing some form of TDD/unit testing. With IntelliJ, I can easily set it to watch for changes and cycle one unit test while I make changes. If something weird happens I can just drop a printf in there, understand and rectify the issue, then take it out. Much faster than step through debugging.
About your debugging from the beginning: with Intellij on the jvm one can "drop frame", which is basically to discard the current function and start over with the stack as it was. Since I mostly write kotlin my objects are immutable, so rerunning most stuff actually works fine. And hot-swapping the function while the debugger is paused I can even try multiple implementations without having to rerun everything, just drop frame, hot swap, step into the new and updated function.
I'd say knowing the debugger well and using it is a faster way to iterate than not.
Hot code replacement, which I have already and still use since 2005 works very well as well.
In php, debugger are half as good but code replacement works immediately.
I would rarely use print debugging and in Java never.
I totally agree but for me that means using a debugger and make full use of its features.
> But if you missed the moment, you have start again from the beginning
As already mentioned in another comment, "drop frame" is a standard Java debugger feature. You can easily go back to the start of any method and go though everything again (side effects of already executed code can give some trouble though).
> Or maybe you're looking at the wrong part entirely at this stage, and just wasting time.
You have the same issue when printing in the wrong parts. Of course you can plaster the code with lots of print statements to see which gets executed. But you can do the same with breakpoints and see where the debugger stops.
> With print debugging you write a bit of code to test a hypothesis. Then you run it, and you keep running it, and especially if it's an UI program you play with the UI and see how the values change during that run.
I really like conditional breakpoints for this. You write a condition for a state that interests you. Then play around in the UI until it stops for that condition and you can easily inspect the complete state at that moment. This is quite useful for debugging methods that are executed very often. Trigger breakpoint (which disable all other breakpoints until they are triggered) are also useful in those situations without requiring any code.
> Once you do know where the problem is, and if it's not apparent what the problem is (most problems are pretty trivial once located), that's IMO the time to break out the debugger and slowly step through it. But the vast majority of problems are faster to solve through rapid iterative exploration with prints in my experience [...]
I can just say that I usually locate issued way faster with a debugger. "rapid iterative exploration" could also kind of describe my workflow using breakpoints. Maybe it actually less about the tool and more about your approach for locating issues in the code.
Right. That’s why printf debugging sucks.
If you’re in a compiled language with a 2-minute iteration it can take an hour to do a binary search to track down an issue that would take 5 minutes with a proper step debugger.
Print debugging is great because it works and is the ultimate fallback. But it sucks and I hate when I am forced to use it.
Edit: ..and do it in production
For over ten years of commercial work I used a debugger only a couple of times and in most cases it was against someone else's code, usually when things were completely broken and I needed to get backtraces from multiple deadlocked threads or lacked debugging symbols and things like radare were also required. There were also times when I manually called a syscall using gdb.
My opinion is that if you can't reason about the code helping yourself with just a couple of additional messages the code is probably broken/too complicated to begin with and requires serious refactoring. I've never understood people stepping through a program hoping to find some mysterious creature somewhere along a huge stack of calls. In my career I have often seen people always debugging an application as a whole instead of separated modules. Dividing a problem is the key. The same key that allows me to still program using vim without autocompletion, keep APIs sane and coherent, and avoid dead code.
One really useful exception is when dealing with electronics. My friends programming hardware use debuggers all the time and in this case it actually makes perfect sense because there is no way to print anything and things like hardware interrupts come into play.
The article hits the point of print debugging, you get to see the backward in time.
By the time you hit "the problem", the pointer is NULL, the memory is trashed, the system is deadlocked, etc. You need to reason about how you got there.
There is a reason why the next step up from basic debugging embedded is "streaming trace"--effectively print on steroids.
I just told one of my co-workers last week that I was going to print-debug an issue. He paused for a moment before saying, "Uh, I can just debug this for you if you like."
So yeah, there's definitely some kind of stigma against print-debugging.
The vast majority of code I investigate is "someone else's" code. Most of the cases, it's a historical accumulation by multiple authors. If you generally only work in your own code, that's quite a different experience, and debugging is generally easier (because you were there when it was written).
The big thing here is that you seem to only work with your own code, where you can arbitrary refactor it and keep the entire thing in your head, as well as quickly find which module does what. But when working with a large foreign project, none of this works. You have to start working at the scope of the entire program, because you have no idea of the internal structure yet. Of course, people who use debuggers divide the code up as they go, but the point here is that they place a few choice breakpoints at central points in the application logic, inspect the stacktraces when one gets hit, and use them to further dig in to the part of the code they need to look at.
Not at all. Due to lack of documentation I look at code of foreign libraries and applications all the time to check what really happens inside and what are the guaranties. The latter being often the case when it comes to concurrency problems.
I don't begrudge people having their own approach to things, but almost universally when I see people use print debugging they seem to take quite a bit longer than just break pointing at the problem area.
If your code is in an unexpected state, it's much easier to hit a breakpoint, examine local values, and then backstep through the call stack to see what went wrong. I dare to say that in a single threaded context, it's almost objectively more effective.
Versus the alternative of using printlines, you basically need to map/model the state flow out in your head which is prone to error (limited capacity of human working memory).
Is it not easier to directly see the problem rather than doing mental math to make assumptions about the problem? I can't see a case for that being more effective.
Most of the time I see people print debugging it seems to be because they haven't used the debugger much... either they aren't comfortable with it, or didn't bother to set it up, or see the mental mapping approach as more "mathematical/logical"... or something. Takes you back to the school days of solving algorithms on paper :)
That being said for simple problems, I've used print debugging myself (again, usually because I'm too lazy to setup the full debugger). Or for multithreaded contexts etc, where thinking it through can actually be more effective than looking directly at the problem (multiple contexts)
Could it be... because they don't know where the problem area is yet? Which is what the original article and most comments in favor of print debugging say.
e.g. in a UI context, timeline shows wrong data. Start there and work backwards.
I can only imagine it's hard to pinpoint if the code is not factored well.
There is a decided lack of academic success in engaging with debugging as an object that can be studied. There are channels to learn about debugging as a stand-alone topic. Programmers don't often talk about debugging techniques in my experience.
For something that takes up the overwhelming bulk of a developer's time the silence is in many ways deafening. It may be that nobody has a method superior to print debugging.
[1] https://docs.microsoft.com/en-us/windows-hardware/drivers/de...
Because the answer is “it depends”. As you mentioned it’s a lot about individual style and preference. I have seen a lot of good coders that got things done but worked in totally different ways.
Debugging requires deep understanding what you are doing and your system. It's different every time. And while there's a general algorithm you can follow:
1. Guess what's wrong
2. Ask "How would I prove that's wrong?"
3. Try it
4. If bug found, fix, if not go back to 1
How would you teach that other than asking people to go solve a bunch of real world problems in real world systems for a few years?
I think this is exactly right. Teaching 1 is impossible I guess but the general method (it's basically the scientific method in an ideal environment) seems teachable.
Come up with a hypothesis of what's wrong, try to prove or disprove the hypothesis.
What output do you expect?
Ask this yourself before starting the debugger. Without this, it is very easy to glance over the point where things go funny.
The same seems to apply to debugging. A student needs to be introduced to the basic concepts and commands, and then practice. Just same as with a writing exercise, the instructor can have specific problems to practice specific techniques.
Do a bisect instead. The complexity is O(log n). It's probably slower than if you guess right on the very first time, but that's less important. Debugging time is dominated by the worst cases.
1. Do something you're 90% sure will work that's on the path towards your actual goal.
2. If it works, move forward in complexity towards your actual goal. Else, move halfway back to the last working thing.
3. When you've trapped the bug between working and non-working to the point that you understand it, stop.
"The weather data isn't getting logged to the text files. Can I ping the weather servers? Yes. Can I do a get on the report endpoint? Yes. Can I append to a text file? Yes. Can I append a line to the weather log file? No. Ok, that narrows it a lot."
The real point of this is that you should spend most of your time with working code, not non-working code. You methodically increment the difficulty of tasks. This is a much more pleasant experience than fucking around with code that just won't work and you don't know why. Most importantly, it completely avoids all those times you wasted hours chasing a bug because of a tiny assumption. It's sorta like TDD but without the massive test writing overhead.
A modification for the disciplined: give yourself one (1) free pass at just taking a stab at the answer. This saves time on easy fixes. "Oh, it must have been that the country setting in the config file is off." Give it a single check. And if it's not that, go back to the slow and steady mode. Cause you don't understand the system as well as you thought.
I would agree that hoing from less to more complexity is a great heuristic for making those guesses on what to check.
I recommend http://debuggingrules.com/ - it's a good book that lays out some rules that have always helped me. When people come to me for help debugging something, invariably they've skipped some of these concepts, and applying them usually gets to the bottom of things faster than randomly changing things (which seems to be a common, but ineffective, way to debug a problem).
UNDERSTAND THE SYSTEM
MAKE IT FAIL
QUIT THINKING AND LOOK
DIVIDE AND CONQUER
CHANGE ONE THING AT A TIME
KEEP AN AUDIT TRAIL
CHECK THE PLUG
GET A FRESH VIEW
IF YOU DIDN’T FIX IT, IT AIN’T FIXED1. Pick a spot in the code
2. Figure out what you expect the state to be there
3. Check and see that the actual state matches
You can kinda binary search your way to the location of a bug by looking in different places
This assumes that I'm working with an already working system, but if I'm implementing a new feature, debugging is easier since I have more flexibility to explore alternative approaches.
It isn't the bulk of my time.
Most of my time is spent figuring out what to do.
Once I have decided that, telling the computer is usually straightforward.
> Programmers don't often talk about debugging techniques in my experience.
No they don't, but look at it this way: Bugs are mistakes, and nobody wants to be told they're making too many mistakes. Anyone who discovers some amazing debugging technique will struggle to share it with anyone else for a lot of reasons, and this is one.
> It may be that nobody has a method superior to print debugging.
Print debugging always works. Getting "a debugger" to work isn't always easy, and if you aren't already comfortable using the debugger to track down the kind of bug you're facing, you will find it very difficult to find the bug and cure it faster than with print debugging. And since people don't tend to make the same mistakes over and over again, debuggers tend to have a very limited utility in those few mistakes made frequently.
My experience is that mistakes like that are the fault of some kind of fundamental misunderstanding, and rather than spend time to learn all of the different fundamental misunderstandings that the debugger was designed to work around, time is better spent simply correcting your misunderstandings.
As far as teaching debugging, it is one thing to show some examples and another one is to run into a bug yourself and get from having no idea how to debug to actually fixing it. That whole experience is hard to replicate in unnatural ways.. When I was in school they told me not to worry too much about debugging and that I'd run into issues in the real world and figure out ways to debug depending on the system and that turned out to be quite correct.
Print debugging is fast in many cases and requires little mental overhead to get going.
But for some/many systems, there's a huge startup and cooldown time for their applications - and compiling in a print, deploying the service, and then running through the steps necessary to recreate a bug is a non-trivial exercise. Think remote debugging of a deployed system with a bug that requires select network and data states that are hard or impossible to replicate in local/dev.
For things like this, being able to isolate the exact point of breakage by stepping through deployed code, and doing immediate evaluation at various points to interrogate state can't be beat.
This post strikes me as either (a) a younger programmer who still thinks that tool choice is a war rather than different tools for different jobs (b) someone making a limp effort at stoking controversy for attention.
I'm not sure what about the article makes you think either a or b. They are trying to critically examine why some people reach for print debugging first, and I think it's spot on.
It possible depends on the kind of programming you do -- I find myself doing little bits of work on projects in many languages, so learning how to get the debugger going often takes longer than finding + fixing the bug.
Record-and-replay debuggers like rr [0] (disclaimer: I started and help maintain it), Undo, TTD, replay.io, etc address one set of problems. You don't have to stop the program; you can examine history without rerunning the program.
Pernosco [1] (disclaimer: also my baby) goes much further. Complaints about step debuggers (even record-and-replay debuggers) only showing you one point in time are absolutely right, so Pernosco implements omniscient debugging: we precompute all program states and implement some novel visualizations of how program state changes over time. One of our primary goals (mostly achieved, I think) is that developers should never feel the need to "step" to build up a mental picture of state evolution. One way we do this is by supporting a form of "interactive print debugging" [2].
Once you buy into omniscient debugging a world of riches opens to you. For example omniscient debuggers like Pernosco let you track dataflow backwards in time [3], a debugging superpower print debugging can't touch.
rr, Pernosco and similar tools can't be used by everyone yet. A lot of engineering work is required to support more languages and operating systems, lower overhead, etc. But it's important to keep in mind that the level of investment in these tools to date has been incredibly low, basically just a handful of startups and destitute open source projects. If the software industry took debugging seriously --- instead of just grumbling about the tools and reverting to print debugging (or, at best, building a polished implementation of the features debuggers have had since the 1980s) --- and invested accordingly we could make enormous strides.
[1] https://pernos.co/about/overview
speed and simplicity
"Simplicity", sure ... it's difficult to beat the simplicity of not using tools.
I'd love to use pernosco, but it's too expensive for me. Do you have any sort of student discount?
Thanks for the cargo-rr crate, that looks nice.
Pernosco's tool is described pretty well on their website, but basically it allows you to view a program inside and out, forwards /and/ backwards, with zero replay lag. Everything from stack traces to variable displays (at any point in time in your code execution) is extremely easy to view and understand. The best part is the lightning fast search functionality (again: zero lag).
On top of this: extraordinary customer service if anything breaks (in my experience, they fix bugs within 24 hours and are highly communicative).
If you value your time I highly recommend you check out this tool.
Something like: func1(4) > func2(null) debug;
Semantically: upon func1 called with arg 4 and some descending path that calls func2 with arg null, enter the debugger
Neat idea!
I’ve heard there’s a culture in parts of Google where kids go through uni using GDB because “Woo Linux!” then go straight into Google where everyone is “Woo Linux!” (I do like Linux, btw) so they are either still using GDB, or more likely have given up on it and reverted to printf. So, everything takes forever to figure out and that’s just “normal”. This was coming from a console gamedev who was shocked by the transition after moving to Google.
Meanwhile, I’ve spent a good part of the past couple decades debugging large volumes of code that I will literally only see once ever. With a good debugger, that can be done effectively because watching and even modifying the code’s behavior can be done at a glance rather than a re-compile.
I’ve also worked on a very big project that used extensive logging because they had a very bad debugger setup and productivity was in the toilet compared to every other job I’ve had. The only way I could keep productive was to take the time to break out systems into small independent programs in my own environment so that I could use a debugger on that rather the run the code where it is.
Log heavily, and log systematically - imagine you're going to need to grep through days of logfiles to find the needle in the haystack - you will eventually. Build in runtime switches to dial log verbosity up and down. Err on the side of providing more context than less. If something throws exceptions, catch them, log exactly where it was, what it was supposed to be doing, and any relevant parameters or state.
If you can get them, process dump files are unreasonably effective, too.
... and it's still a royal pain to get your program to display stuff on some text console when you want those printfs.
Also, you have to use Windows. I'd rather avoid that if i can.
I'm still waiting for the feature where you can conditionally stop at some breakpoint -only- if some other breakpoint/watchpoint was crossed over. It's not a conditional breakpoint, because conditional breakpoints can only watch variables, not other breakpoints. You could of course set some variable depending on whether some section was entered and then conditionally break based on that variable. But then you're back to print debugging land, having to manually insert code in order debug the program.
Debuggers are superior when it comes to interrogating the exact state of some variables, as well as the decision paths the program takes. For anything simpler, print debugging simply offers the better developer experience.
PyCharm 2021.1 has this, so I would guess that other members of the IntelliJ family probably have it too.
Set a breakpoint and then right-click the red dot, and click More to open the full Breakpoints dialog. Open the drop-down under "Disable until hitting the following breakpoint:" and select the other breakpoint that should enable this one.
And thank you for mentioning this! I didn't know PyCharm had this feature until I took a look after seeing your comment. This will be super useful.
I stop at a breakpoint only after another breakpoint is hit all the time. You set the first breakpoint and run the program. It gets hit and pauses, you set the second breakpoint, then resume.
I'm just not getting how print debugging is the better experience.
No offense, but this sounds like you just really need to learn how to use a debugger - This is in no way a "typical step-through debugging session." I've been a professional software developer for 16 years and I've never once in my life "spamm[ed] step-over/step-in"
>I'm still waiting for the feature where you can conditionally stop at some breakpoint -only- if some other breakpoint/watchpoint was crossed over.
This is trivial to do, place two breakpoints, disable one. When the breakpoint is hit, enable the second (and optionally, disable the first).
I think the two methods are complementary and should be use in combination.
However, the big issue is that basic printf debugging is very simple to use and debuggers have a steeper learning curve in the beginning. Therefore, people start using printf debugging and don't invest into learning how to use debuggers. And when developers don't invest into learning how to use debuggers properly, they are missing the skills to utilize them and still use printf debugging in cases when debuggers are clearly superior.
This should be repeated many times. I am getting very tired of the constant need of people who want to have a strict ideology and find the one true way of doing things.
That said, most people don't know this is possible (the learning curve issue you mentioned), even though it's an important part of how to use debuggers!
In most cases I prefer to do something trace-based, and in the IDEs I've used the debuggers have much weaker support for that than they do for stepping around.
In particular, setting up tracepoints tends to involve fiddly dialog boxes which are much less convenient than using the main text-editor interface to say what you want.
I think there's plenty of scope for debuggers to provide a better interface for trace-style debugging. For example I'd like to be able to toggle a tracepoint after capturing the run, and have the lines it created appear or disappear, or add a filter expression or additional information to display without having to rerun the program.
That's why 'I must' use print debugging, because the 'powers that be' still provide a broken, half-baked solution 30 years in.
Print debugging is however so powerful, I think there almost should be a mechanism built into languages and tooling around it so that it becomes part of the process instead of a 'kind of workaround'. It's something we all do, constantly, and yet you'll never hear about it when people are arguing about Rust or Go.
There is "SEND" which can be used as an ad hoc comms channel, aimed at a program method.
And a debug system that can both stream data, text and graphics to a client, as well as capture and report on the state of the 8 CPU cores possibly running.
https://www.parallax.com/propeller-2-graphical-debug-tools-i...
The debug output is something like using an xterm with Tektronix emulation turned on, and with all the lower level bits packaged away. The use can do a lot, from a "print" type operation to sophisticated graphics, static or animated.
On the capture side, a sort of supervisor region of RAM is reserved to for an ISR to capture processor state, or anything in memory really. Can be time, or event driven.
I need to see big picture, whole state, all the stuff and rapidly jump back and forth. I also, supposidely, have ability to keep a lot of state / scope/ abstraction in my head. So I find print debugging sufficient and fast. Rarely encounter situation I feel need for "stronger" tool.
Where other people focus on one thing, all that simultaneous output is just noise and distraction to them. And based on the continued use and popularity of step-based debuggers, these people are much more productive (and happier) using those type of tools.
It's very important to understand neither system is inherently superior. Although one or the other is superior to each individual. [btw over 35yrs of tech industry / software development I've found this true, that tools/paradigms are not universally superior but are superior based on individual) for many subjects. All the ones that have internal debates in techdom]
A couple of years ago, I had to maintain a bit of Java code using Eclipse. That is, the old IDE everyone loves to hate. And while some of that hate is well deserved, for debugging, it was the most pleasant experience I had in a long time. Nice object inspector, edit-and-continue, conditional breakpoints, and step-by-step that works. Much better than fumbling around with GDB or one of its less-than-perfect UIs.
Also note that printf debugging and the step-by-step and breakpoint kind are not mutually exclusive. With an edit-and-continue feature, you can get the best of both worlds, but that's not something common these days, unfortunately.
ALternatively, your printf can use the wrong formatter string, and cause unrelated crashes. Such joy!
Makes me nostalgic for the good old days.
What compiler are you using? Aztec C? Prehistoric C?
For a lot of glue type code, I don't actually care about stepping through something line by line. I really want to see how components interact, not each step of execution. Though I do wish languages had better support for doing something like printing out all local variables in the current function along with the stack trace, sort of like a very shallow, low-cost dump.
Another big advantage is that logging is usually much easier to turn on (or even keep on by default) for production scenarios. Good luck getting some bank to let you run a debugger or even get a dump for anything.
Breakpoint at A, breakpoint at B, both automatically continue when hit.
In both, you can't retroactively debug already executed code.
This is one of the areas where I'm really proud of what we did in Dark. In Dark (https://darklang.com), all execution is traced and you can see the value of any expression on any trace by putting your cursor in the expression. Advantages:
- no struggle to reproduce the error
- no need to set up a debugger
- no need to add print statements
When I write Dark, I can debug in seconds. When I work on the Dark implementation (F# or ReScript), I spend at least minutes on each bug because I need to do a bunch of setup to find enough information to diagnose the error.
Python: if something: breakpoint()
Js: if (something) debugger;
Much easier than breakpoint conditions in visual debuggers imho.
I would love to have a debugger that offers a partial text editor experience, eg. it shows my code, I move the cursor to some statement, then I press some key binding and the debugger starts printing (in another window) all the state changes in that statement. Another key binding prints all the state changes in the entire function, etc. All of this while the program is running.
Are there debuggers that can do this? I have used gdb in the past, but having to set up breakpoints by hand and remembering names makes it too tedious.
Print debugging excels at triaging the problem. And every language has print statements. Ubiquitous first tier support. They help you narrow down where your assumptions about the program behavior may be wrong.
Once you know what area to focus on, you pull out the debugger and step thru the code.
All output goes to /tmp/q (or on Windows, to $HOME/tmp/q). You can watch the output with this shell command while your program is running:
tail -f /tmp/qIf I have a bug I can reproduce I can write a unit or integration test, try narrowing down the issue and use a debugger on the test itself for further help. Intellij has great support here, VS as well and there's plenty others.
If the bug exists in production only using a debugger I can connect to it remotely and dump the state (thread dumps in Java or core dumps with Delve for Go). If there's an option of using a profiler it makes the experience even better especially for diagnosing performance issues.
For distributed systems monitoring libraries, log aggregators are much more useful than raw logs. Proper metrics allow fast pinpointing of issues and log aggregators give me an option to either look for rare/common errors easily.
The only case I'd resort to prints nowadays is as a last resort if there are no better options.
I dont understand how printing text could EVER approach this given I can test my assumptions right away and generally only need 1 error to happen to understand the totality of the circumstances.
I've caught quite a few bugs using this show-me-all-locals() approach...
What I liked about pysnooper is the ability to snoop on a specific function and/or code block to focus on the work-in-progress section of code.
Logging is much nicer because you can turn the exploration process into a text analysis problem. Logs can be searched, stored and compared. For me, sifting the log is much easier.
Whenever I try to write a medium-sized program for a serious kind of purpose, the first thing I do is to set up a nice and reliable logging system. This is the decision that you won't regret for the rest of development.
I would argue that the use case of a debugger is much narrower than logging/printf debugging.
One would assume that anybody who used a debugger for more than a day knows about breakpoints. TFA isn't saying you have to step through every line in a debugger.
It's saying that, even if you employ your amazing debugging skill to find exactly the point you want to look at, you will only be looking at that exact point in execution, and not other points at the same time. Sure, it will be a very detailed representation of that particular point, which can be extremely handy, but sometimes you want to look at a hundred different points in execution, at once. That's when printf comes handy - you just need a large monitor (or a small font and good eyes).
I think a big part of the issue is that printf debugging has always been "good enough" for me. I have used gdb in the past, but I've never felt the incentive to become good at it, so my knowledge of it atrophies and it has become a less interesting option over time. On the other hand, my knowledge of how to printf messages and extract them from the running process never atrophy because I do exactly that every day.
So maybe the situation changes if ever I come across a bug that's so mindbogglingly convoluted that printf debugging is not viable. Then I'll be forced to learn to use a step debugger well, and that could change my choice of tools going forward.
For me is faster to double click the border of a line in VS or writing "break 123" or "break fooFunction" in GDB and stepping and watching how some values changes than adding and removing "printf" lines.
Adding some asserts are other thing. They always are good, and often necessary to find some "Heisenbugs".
In other languages I probably won't think the same, but I haven't done anything big enough outside C or C++ to give a proper opinion.
At Google, we have time-traveling debuggers neatly integrated into our cloud IDE: You can step forwards and backwards, you can inspect variables for all the values they've had or will have until program termination, and you can also see all invocations of methods (along with their parameters) that have happened or will happen.
I still use logging for debugging. Cool tech aside, I think what you really need, above everything else, is the fastest possible iteration cycles.
I also don't see any contradiction between liking good logs and using the debugger when needed.
When people say they use 'print statements,' are they talking about log points (a debugger construct), logging or something else?
I should hope that, in most cases, they're not literally modifying their source code to achieve this. While there are a handful of scenarios in which this is necessary, on the whole it strikes me as inefficient, time-consuming and error-prone. In most environments, there are better ways to make this data observable.
I can't understand why would anyone prefer to write some print, when you can have Visual Studio's
* break point
* conditional break point
* ability to place another break points when you're already on the other
* expression evaluation at fly!!
* decent possibility to modify code at fly
I still remember case where I modified function with line with bad SQL (breakpoint after executing this SQL), added call to the same function with the same parameters after this breakpoint, let it execute again, caught the breakpoint once again and removed that call to itself
and all of that without recompiling program! it felt like magic
One breakpoint breaks at one thing, that's why you can have many of them
I would put forward that proficiency with this style of debugging (closely related to useful performance profiling) is a major factor separating mediocre programmers from the quasi-mythical 10X rockstars.
- cross language/platform
- forces you to come up with hypothesis up front, then test these very systematically
- you know what you are doing
- debugger doesnt interfere
- works across threads and processes
• Nearly everything is immutable, so once I print the value of an expression I know it won’t change in the future. This is not the case in other programming languages, where a variable can be mutated after I print it.
• The base library provides a really nice range of functions for print debugging [0] — so I can just wrap any expression I want printed in ‘traceShowId’, and it’ll get printed. (Yes, these functions break purity; that’s why the module is marked ‘Debug’!)
Of course, sometimes print debugging isn’t sufficient, in which case I fire up the GHCi stepper debugger. But for the vast majority of cases print debugging works well.
[0] https://hackage.haskell.org/package/base-4.15.0.0/docs/Debug...
Debugging on the otherhand, well.. I've just been told by my senior to write bigger functions, because the line-by-line debugging tool jumps around too much when moving between functions to functions.
Pernosco offers the best of both worlds ( debugger, print), along with a few magical features.
With it you can print anything present in your recording and step, and do anything you'd do in a regular debugging.
The video shows the user using their mouse and typing the expression to be printed into a tiny text input.
Part of the attraction of print-style debugging is the convenience of being able to use your main editor UI, along with all the conveniences that provides, to write that expression.
(That might be fancy completion or vi-style editing commands or keyboard macros; it will be different for different programmers.)
If I'm honest, in spite of it not being perfect, it's much better than regular printf. The other very nice feature is dataflow (click on a variable value, and it tells you where it comes from, and it handles copies seamlessly), which makes a large number of debugging tasks trivial.
From there there are situations where a debugger will save a LOT of time. I'm thinking of trying to figure out what's causing a behavior in a large dependency injected application with plugins when you have little to no familiarity with all the code involved. And then of course all the other things a debugger can do for you.
> Clearly Real Debuggers offer a superior experience to print debugging in so many ways. But print debugging is just easier to get started with, and it reliably works anywhere, so that’s why we use print debugging so much.
I think the tone of the first sentence and the word "superior" unnecessarily creates a strawman.
So I tend to write a LOT of print statements that flush of debug variables right before I where I want to debug. Then I set a conditional breakpoint so that I can have the logs "stop" right where I want the program to.
Example:
// debug print
let someValueICareAbout = variable...
print(someValueICareAbout)
print("") <- conditional debug point here "if someValueICareAbout == 3"
I think it's technically still "print debugging", because I'm only using the debugger to stop the program so I get a chance to read my output.
It’s an AST interpreter so sometimes I want to see the value of something that’s 5 properties away inside a syntax node
That being said, there's almost no good reason for a platform to not support step-wise debugging, so it's a big code smell that you're going to have a bad time in general there (even if in practice you'd largely use printf anyway).
Try Visual Studio under Windows. Go on, try it. You'll be surprised at just how stone-knives-and-bearskins the standard tools on Linux really are.
It’s a workflow that makes “Wait, why did that happen” such an easy question to answer.
I constantly use both together. For problems that a quickly and reliably reproducible I'll often just use the debugger (if rr is suitable, even better).
But there's plenty problems that take a while to reproduce, involve many threads / processes, etc. Where the initial set of potential issues is too wide to easily target with a debugger. There sprinkling printfs around can provide data at a lower overhead than doable with a debugger.
Just yesterday I was debugging something where rr didn't finish replaying a workload that originally takes 10s within an hour (loads of io). Switching to print debugging I pinpointed the issue in < 10min.
I've not seen anyone else try anything like this. There's a YouTube demo here:
Shameless plug: If you're writing rust I wrote a tiny wrapper that finds the appropriate binaries and provides the right config to make it as easy as `cargo rr test my_test`. https://crates.io/crates/cargo-rr
Maybe it is just the way my brain works? I'd rather stop and see what I need behind a condition than have to filter through a lot of possibly unformatted console output.
The only time to really beware is embedded and real time systems where printing can throw timing way off or cause other side effects.
I heard of a case once where printing via JTAG caused an issue due to the power draw of sending all the extra data out. But that was trying to debug a novel board design and its software at once.
You won’t hit that kind of thing on normal computers like desktop, mobile, or cloud unless you are writing drivers.
A debugger is for when you want to inspect local state in detail. That can indeed often be very useful, and they are sophisticated technology.
However, the people who think that a debugger is the only way to debug just aren't good programmers: often you want a picture of the overall behavior of your program. As has been said by someone other than me, a debugger allows you to fix a bug; print statements allow you to think about the right fix for a bug.
"debugger" := a thing that pauses and allows you to inspect the local stack frame, and step into/over, evaluate code in the frame context, etc.
"print statements" := any technique involving letting your program run to completion and having it output debugging information to screen or file for examination after it has finished.
Sometimes I do some Java work though and I usually end up going to print debugging because trying to figure out all the Java logging framework, or not ending up like 40 layers deep in some magic framework dependency that is interecepting my code which is what always happens when I use a debugger.
That being said do those who work in compiled languages make more heavy use of debuggers?
I work in both quite a bit. I actually think I end up using a debugger more in e.g. Python because I'm more often asking questions like "what is the type of the thing being passed here", which is not a thing I need to seek out in something like Go.
That said, I think it's more a style difference than anything. I use debuggers in both compiled and noncompiled languages when I need a deeper look, and I'd guess people who don't use debuggers in scripting languages wouldn't use them in compiled languages. Probably also has to do with the ecosystem and how easy/effective debuggers are.
In C# I'd almost never use print debugging and the whole thing seems ridiculously antiquated (it's partly why I hate JS work). [Assuming you're working in Visual Studio...] You literally hit 1 key, F5 and then you can step through, time travel, edit and continue. I wonder if people just haven't experienced the ease of debugging in .NET with VS. I'd say I write probably 50% of my code in a debugging session, edit and continue is a game changer.
I did a little Java work in Intellij and it was similar but I think partly due to a lack of familiarity with the UI didn't feel quite as powerful.
They also let you place a breakpoint which doesn't stop execution so you can get a stack trace, variable states etc. without the pain.
Maybe it works better in non-C++ languages.
`{ ... }?` denotes a scope we want to inspect, debugger gets launched and we get a generic reification of the tree path at that point; with ability to tweak parameters up that path and see multiple new trees rapidly (think Brett Victor live coding)
Honestly I think printf debugging is a pity. I do it.. but it feels like processing xml with sed.
Used gdb scripts in the past to make debug sessions repeatable. Stuck beyond the point your interested in? No problem, just restart the session with your gdb script and your right back on track! You can also add custom functions to output your state in a more meaningful way or to mock some state. In longer debugging sessions, a good debugger can be a life safer!
Still, for shorter sessions, reading logs and adding occasional prints are hard to beat.
I tried using debuggers, but it was always too much hassle.
What is really effective is "visual debugging". Say for example you are testing for bias in an RNG. Rendering a large format image of random rgb values will immediately show any cycles, even to the untrained eye.
Consider GPGPU workloads, for ML or ray tracing for example. There are myriad levels of variables to track: resources, allocations, command buffer state, synchronization, compute kernel per vector, and so on. All primitives that very much lend themselves to graphical representations!
Right now editing live code in a profiler usually involves textual editing of the graphical shaders. But it's easy to see how this evolves to a purely visual shader editor, not unlike those found in Unreal or Godot.
?
I use debuggers even for print-debugging for this kind of reason. No need to re-compile between changing prints, just re-run - the debugger session will hold them from previous runs, you can temporarily disable them with a single click, etc. It's FAR faster and more flexible.
More languages should have that.