[dmd-internals] Asserts
David Held
dmd at wyntrmute.com
Sat Nov 10 11:23:53 PST 2012
On 11/9/2012 11:38 PM, Walter Bright wrote:
> [...]
> I'll often use printf because although the debugger knows types, it
> rarely shows the values in a form I particularly need to track down a
> problem, which tends to be different every time. And besides, throwing
> in a few printfs is fast and easy, whereas setting break points and
> stepping through a program is an awfully tedious process. Or maybe I
> never learned to use debugger properly, which is possible since I've
> been forced to debug systems where no debugger whatsoever was
> available - I've even debugged programs using an oscilloscope, making
> clicks on a speaker, blinking an LED, whatever is available.
You're making my point for me, Walter! I have seen some people whiz
through the debugger like they live in it, but I would say that level of
familiarity tends to be the exception, rather than the rule. And, it
always makes me a little uncomfortable when I see it (why would someone
*need* to be that proficient with the debugger...?). Firing up the
debugger, for many people, is a relatively expensive process, because it
isn't something that good programmers should be doing very often (unless
you subscribe to the school which says that you should always step
through new code in the debugger...consider this an alternative to
writing unit tests).
> Note that getting a call stack for a seg fault does not suffer from
> these problems. I just:
>
> gdb --args dmd foo.d
>
> and whammo, I got my stack trace, complete with files and line numbers.
There are two issues here. 1) Bugs which don't manifest as a segfault.
2) Bugs in which a segfault is the manifestation, but the root cause is
far away (i.e.: not even in the call stack). I will say more on this below.
> [...]
>> Especially when there may be hundreds of instances running, while
>> only a few actually experience a problem, logging usually turns out
>> to be the better choice. Then consider that logging is also more
>> useful for bug reporting, as well as visualizing the code flow even
>> in non-error cases.
>
> Sure, but that doesn't apply to dmd. What's best practice for one kind
> of program isn't for another.
There are many times when a command-line program offers logging of some
sort which has helped me identify a problem (often a configuration error
on my part). Some obvious examples are command shell scripts (which, by
default, simply tell you everything they are doing...both annoying and
useful) and makefiles (large build systems with hundreds of makefiles
almost always require a verbose mode to help debug a badly written
makefile).
Also, note that when I am debugging a service, I am usually using it in
a style which is equivalent to dmd. That is, I get a repro case, I send
it in to a standalone instance, I look the response and the logs. This
is really no different from invoking dmd on a repro case. Even in this
scenario, logs are incredibly useful because they tell me the
approximate location where something went wrong. Sometimes, this is
enough to go look in the source and spot the error, and other times, I
have to attach a debugger. But even when I have to go to the debugger,
the logs let me skip 90% of the single-stepping I might otherwise have
to do (because they tell me where things *probably worked correctly*).
> [...]
> I've tried that (see the LOG macros in template.c). It doesn't work
> very well, because the logging data tends to be far too voluminous. I
> like to tailor it to each specific problem. It's faster for me, and
> works.
The problem is not that a logging system doesn't work very well, but
that a logging system without a configuration system is not first-class,
and *that* is what doesn't work very well. If you had something like
log4j available, you would be able to tailor the output to something
manageable. An all-or-nothing log is definitely too much data when you
turn it on.
On 11/9/2012 11:44 PM, Walter Bright wrote:
> [...]
> There is some async code in there. If I suspect a problem with it,
> I've left in the single thread logic, and switch to that in order to
> make it deterministic.
But that doesn't tell you what the problem is. It just lets you escape
to something functional by giving up on the parallelism. Logs at least
tell you the running state in the parallel case, which is often enough
to guess at what is wrong. Trying to find a synchronization bug in
parallel code is pretty darned difficult in a debugger (for what I hope
are obvious reasons).
> [...]
> Actually, very very few bugs manifest themselves as seg faults. I
> mentioned before that I regard the emphasis on NULL pointers to be
> wildly excessive.
I would like to define a metric, which I call "bug depth". Suppose that
incorrect program behavior is noticed, and bad behavior is associated
with some symbol, S. Now, it could be that there is a problem with the
immediate computation of S, whatever that might be (I mean, like in the
same lexical scope). Or, it could be that S is merely a victim of a bad
computation somewhere else (i.e.: the computation of S received a bad
input from some other computation). Let us call the bad input S'. Now,
it again may be the case that S' is a first-order bad actor, or that it
is the victim of a bug earlier in the computation, say, from S''. Let
us call the root cause symbol R. Now, there is some trail of
dependencies from R to S which explain the manifestation of the bug.
And let us call the number of references which must be followed from S
to R the "bug depth".
Now that we have this metric, we can talk about "shallow" bugs and
"deep" bugs. When a segfault is caused by code immediately surrounding
the bad symbol, we can say that the bug causing the segfault is
"shallow". And when it is caused by a problem, say, 5 function calls
away, in non-trivial functions, it is probably fair to say that the bug
is "deep". In my experience, shallow bugs are usually simple mistakes.
A programmer failed to check a boundary condition due to laziness, they
used the wrong operator, they transposed some symbols, they re-used a
variable they shouldn't have, etc. And you know they are simple
mistakes when you can show the offending code to any programmer
(including ones who don't know the context), and they can spot the bug.
These kinds of bugs are easy to identify and fix.
The real problem is when you look at the code where something is
failing, and there is no obvious explanation for the failure. Ok, maybe
being able to see the state a few frames up the stack will expose the
root cause. When this happens, happy day! It's not the shallowest bug,
but the stack is the next easiest context in which to look for root
causes. The worst kinds of bugs happen when *everyone thinks they did
the right thing*, and what really happened is that two coders disagreed
on some program invariant. This is the kind of bug which tends to take
the longest to figure out, because most of the code and program state
looks the way everyone expects it to look. And when you finally
discover the problem, it isn't a 1-line fix, because an entire module
has been written with this bad assumption, or the code does something
fairly complicated that can't be changed easily.
There are several ways to defend against these types of bugs, all of
which have a cost. There's the formal route, where you specify all
valid inputs and outputs for each function (as documentation). There's
the testing route, where you write unit tests for each function. And
there's the contract-based route, where you define invariants checked at
runtime. In fact, all 3 are valuable, but the return on investment for
each one depends on the scale of the program.
Although I think good documentation is essential for a multi-coder
project, I would probably do that last. In fact, the technique which is
the cheapest but most effective is to simply assert all your invariants
inside your functions. Yes, this includes things you think are silly,
like checking for NULL pointers. But it also includes things which are
less silly, like checking for empty strings, empty containers, and other
input assumptions which occur. It's essentially an argument for
contract-based programming. D has this feature in the language. It is
ironic that it is virtually absent from the compiler itself. There are
probably more assert(0) in the code than any other assert.
DMD has a fair number of open bugs left, and if I had to guess, the easy
ones have already been cherry-picked. That means the remainders are far
more likely to be deep bugs rather than shallow ones. And the only way
I know how to attack deep bugs (both proactively and reactively) is to
start making assumptions explicit (via assertions, exceptions,
documentation), and give the people debugging a visualization of what is
happening in the program via logs/debug output. Often times, a log file
will show patterns that give you a fuzzy, imprecise sense of what is
happening that is still useful, because when a bug shows up, it disrupts
the pattern in some obvious way. This is what I mean by "visualizing
the flow". It's being able to step back from the bark-staring which is
single-stepping, and trying to look at a stand of trees in the forest.
Dave
More information about the dmd-internals
mailing list