Which in turn was inspired by Haskell s `Debug.Trace` functions [2].
It's definitely a convenient tool if you can't use a debugger or want to follow more complex interactions.
[1]https://doc.rust-lang.org/std/macro.dbg.html
[2] https://hackage.haskell.org/package/base-4.12.0.0/docs/Debug...
print(f"{name=}") expands to print(f"name={name}")
According to the RFC, the only inspiration from Debug.Trace was returning the input. The only other mention of haskell in RFCs 2173 and 2361 is the rejection of `show!`.
You can't learn everything from official documents...
A debugger gives you the state of the entire program, and one point in time. A log allows you to focus, which gives you the state you actually care about, at all the points in time where you care about it.
The time-evolution of the state of the program is practically the only thing I'm interested in.
I find that code that has been written mainly with debuggers is often full of tons of errors, often simple negations which have been worked around an even number of times. Then you're stuck with the debugger, and there's no way to reason about the program. The result is low quality.
Er.... I’m pretty sure debugger advocates aren’t advocating using debuggers without logical thought. Usually, it’s more about that debugger is a useful tool when one spends three hours logically explaining the problem but can’t figure it out.
If you can find all kinds of bugs in ten minutes, then great! You’re officially an endorsed 10x programmer. :-) I can’t, for some bugs, (reproducing bugs in complex programs is a difficult hurdle for me too) so I use a debugger.
> I find that code that has been written mainly with debuggers is often full of tons of errors, often simple negations which have been worked around an even number of times.
Usually, I find that code that is thoughtfully written and extensively debugged in the debugger is the most robust part of the program, but YYMV.
Effective use of printf and breakpoints is, IMO, as a tool in logically thinking through the problem. You work out some belief of the problem, you see how this belief being wrong could cause the bug but you can’t see how the belief could be wrong, so you use debugging tools to check your assumption. This is imo the easiest and quickest way to find something surprising. And once the assumption is either validated or invalidated, it’s time to go back to thinking through the problem logically.
Personally I prefer printf to breakpoints most of the time, because they leave a trail that I can keep going back to when thinking through the problem logically, and because they build up as I work through a problem. I think printf debugging suffers the “trap” issue less for this reason, but sometimes it’s nice to do a little exploratory debugging once the assumption is invalidated.
That’s only if you’re only using the most basic features. A good debugger lets you run dynamic analysis and introspection at the level that static log statements cannot allow.
Simple things that people like reasoning about and visualizing with printf() statements can be done in a debugger more easily after using your brain and logic to think about what the issue might be. Think a value got assigned a value it should't have? You add log statements, rebuild, relink, reproduce; or, you could just set a hardware watchpoint to print the backtrace whenever it changes. In my experience people don't reach for the debugger way of doing it simply because it might not be needed all the time and the syntax is forgotten. Diving into `man` or docs is harder than printf(). Use those skills more regularly and/or make a cheatsheet, macros, etc. and it is less of a problem.
Debuggers must be used in certain situations. Personally, the canonical example I use is tracking down compiler bugs. I and colleagues have been hit with a number and it would be utterly hopeless to attempt to reason about what is going on without a debugger. (One particular past case that stands out was the compiler, under very certain circumstances, not restoring a register that it was obligated to restore when emitting a catch block. Isolating the root cause and generating a reduced example was loads of fun /s, would not recommend.) Then there's kernels, runtime engines with JIT, diagnosing unreproducible problems in situ where the executable can't be replaced or restarted, and the list goes on...
The key idea is that modern CPUs dynamically branch-predict-away if-statements that are rarely/never invoked, such as a dynamically disabled dbg() call.
With the performance difference magnified 10x by loop unrolling, this is what I'm seeing on my Mac laptop:
830000000 iterations in 3 secs = 2.76667e+08 iters/sec (compiled-out). 840000000 iterations in 3 secs = 2.8e+08 iters/sec (dynamically disabled).
And this is the worst case, where the whole program does nothing but call dbg() - real world programs contain lots of other real work, drowning the minute difference in performance, i.e. in a real program I doubt you'd see even 0.1% total performance difference, littering it with dynamic dbg() statements.
p.s. my C/C++ is pretty rusty - feedback welcome, but pls be kind.
dbg.h should support a function that disables writing out to the screen, but still returning the expression during run-time.
You could try it out with something like this:
disable_dbg_output();
int a = 0;
for(int i=0; i<1000000000; i++) {
a += dbg(i);
}The code for dynamic enable/disable is trivial but the design is a bit subjective. For example, another API might be set_dbg_output(bool) and then maybe get_dbg_output() to inspect the current state.
These are going to be limited resources so you'd rather not pollute them and make other branches predict wrong because they are aliases (shared) by many different instruction address.
Also, the fetcher can only get instructions in blocks and if there are multiple branches it will have to predict based on the first branch.
There is a lot of machinery in branch and branch the prediction because keeping the pipeline full of instructions is critically important for performance.
And while throuhput may not suffer much, the chances of increasing mispredictions in the hot path increase adversely harming latency.
These really need to be compiled out for release because you can't assume others can afford the cycles.
Instead, you want:
* A pretty-printer library (possibly single-header) * A function for obtaining the typename; see:
https://stackoverflow.com/q/35941045/1593077
https://stackoverflow.com/a/56766138/1593077
for a constexpr approach.
and it won't hurt to have:* A logging library with log levels (so that you don't necessarily need to recompile to enable these outputs) * A stack trace printing library
When you have that, such a macro becomes nearly trivial. Oh, yeah, and - drop the silly ANSI coloring.
(defmacro dbg [expr]
(println '~expr ~expr)) 1> (defmacro dbg (expr)
(with-gensyms (val)
^(let ((,val ,expr))
(format t "~a: ~s -> ~s\n" (source-loc-str ',expr) ',expr ,val)
,val)))
dbg
2> (dbg (cons 1 2))
expr-2:1: (cons 1 2) -> (1 . 2)
(1 . 2)
3> (dbg (+ 2 2))
expr-3:1: (+ 2 2) -> 4
4
4> (progn
(dbg (list 1 2))
(dbg (cons 1 2))
(dbg (+ 1 2)))
expr-4:2: (list 1 2) -> (1 2)
expr-4:3: (cons 1 2) -> (1 . 2)
expr-4:4: (+ 1 2) -> 3
3
Colorization is just inserting some trivial ANSI codes. Arguably, this just slows down the program even more and increases the size of the log files. It can be done as a post-processing filter.> The dbg! macro works exactly the same in release builds. This is useful when debugging issues that only occur in release builds or when debugging in release mode is significantly faster.
Note, however, that the C++ dbg(..) macro can be easily disabled to a no-op (identity-op, to be precise) with the DBG_MACRO_DISABLE flag.
I got inspired by this HN post and implemented it today. There's certainly room for improvment.
They are less advanced on some areas, but more advanced on some.
Particularly nice features:
* a structured log indented by stack trace depth (fully portable, put a simple macro at function start, will log start and any exit)
* log any expression: `somemacro(foo.bar())` will log `foo.bar() = 42`
* log scopes like functions
* with possibility to jump to source code line on one click or keypress
* browse the log with structured jumps (like step into function vs advance one line, back and forth)
...just by generating a text format that is parsed by common pre-existing tools (namely emacs compilation-mode and a few other short settings).
I've been planning to share that for a moment. It will eventually appear on https://github.com/fidergo-stephane-gourichon?tab=repositori...
I wrote a similar set of macros for C, see my comment https://news.ycombinator.com/item?id=21071497 on eerimoq's "Show HN" https://news.ycombinator.com/item?id=21040649
When the debugger is too cumbersome to use good old prints is always a nice fallback.