The Use of assert() in SQLite
sqlite.org
sqlite.org
The Go philosophy against assert() and assertion libraries is largely that the error messages produced by assert() are low quality. The idea is that you should not just have a stack trace, you should have a good error message. The assert() macro can give you a stack trace with some effort, but rarely gives you a good message. Good use of panic() is easy and gives you both. It’s also used mainly for runtime invariant checking, although there are some other ways it can be used (since it can be caught).
asset(condition && "static reason why we expected this condition to be true");
Unfortunately, this doesn't allow for a dynamic string, but does generally present a message about the assert. Also, if properly configured, a failed assert results in an abort and should generate a core/crash dump. Pretty useful to be able to load in a debugger for further inspection.But to the point about provide a good message: that's on the developer, and its going to be true no matter the language.
if (argc != 2)
throw std::runtime_error("Expect 1 argument. Actual "s + std::to_string(argc - 1));
Don't get terrified by exception. If you don't catch any exception, then it is mostly abort() with dynamic string^ semi-debug in our case means minimal optimisations and debug asserts enabled.
why do you care about the runtime cost of something that makes your program crash ?
Let's pretend my previous post started with
"In most cases, "
I think both parent commenters (htfy96 and Asooka) got on a tangent because htfy96 substituted a throw exception for an assert() in gp's (hermitdev) code -- as if they were equivalent. However, exceptions have fundamentally different semantics from asserts and are not substitutes for each other. (For anyone unfamiliar with the different purposes of exceptions vs asserts, please read the top 2 answers in this Stackoverflow question.[0])
Things like array bounds checking (used as a runtime safety check instead of an "impossibility" check) would not be tested with assert().
To answer your direct question, programmers who use assert() correctly for "testing their understanding of the world by testing an impossible condition" don't want to pay a runtime penalty for it.
[0] https://stackoverflow.com/questions/1467568/debug-assert-vs-...
As far as I'm concerned, assert() has two jobs, both essential: Killing the program AND giving an error message which gives the developers knowledge of precisely where the deadly assert() is. Doing only one of those things leaves the job undone.
The article very clearly says that (unlike normal C behaviour) it's a no-op in release builds of SQLite, because otherwise they get a ~3x slowdown. Does Go compile away those `if` statements?
The HN guidelines specifically request that we not make comments like this.
> Please don't insinuate that someone hasn't read an article. "Did you even read the article? It mentions that" can be shortened to "The article mentions that."
I was about to delete my comment since it was no longer relevant, but maybe I will leave it here since your reply and edit are such a great example of how to be constructive and civil. (OTOH, let me know if you'd prefer for me to delete it.) Thanks!
https://www.zdnet.com/article/sqlite-bug-impacts-thousands-o...
This stuff doesn't happen in Go and Rust programs.
I'd rather have Go's guarantees than C's.
As an example, checking errors on GPU is enormously slow because you stall the CPU until the hardware FIFO drains every time you do. You can't ship software that checks for errors after issuing each DirectX or OpenGL call. But writing graphics code is also really annoying without a debug mode that checks for errors after each render command, because the failure mode of graphics programs tends to be "black screen" or "screen full of garbage".
Baking that into the language seems like it just replaces Go with that meta-language and hides the Go underneath like how "C" is really CPP-lang with a C compiler underneath.
That said, you can do this easily enough in Go. Define a package:
package debug
const Debug = true
func Assert(cond bool, msg string) {
if Debug && cond {
panic(msg)
}
}
Have your build system generate the file that contains Debug = true. In optimization mode, make it Debug = false. As it is const, the compiler will propagate it, Assert will be inlined and the dead code removed. If you don't like relying on the compiler, have your code generator generate empty Assert functions. If you are working in a very bad code base that catches all panics, you'll need an extra 5 lines of code in the implementation of Assert, and your stack traces won't be pretty. (Roughly: put the condition check in a goroutine, block the Assert call on a channel signal from that goroutine.) I'd recommend the first project be to remove those panic catch-alls.But really, all of this is only appropriate when you are far out in the weeds. Ship your program with those asserts. If they are too slow for production they are too slow for development.
https://play.golang.org/p/Ts6QqRrWwuQ
I couldn't get that to not trigger the lock.
[later] I could get it to remove it if you pass in a func instead.
(1) One test uses compiler and preprocessor options set up to measure coverage. This is not so much a test of SQLite itself as it is a test of the test logic, to verify that the testing provides 100% MC/DC.
(2) The second test runs with assert(), ALWAYS(), and NEVER() enabled. This is like a unit test. It verifies internal assumptions and state, at the expense of running 3x slower.
(3) Finally, we build as for delivery and test once again. This is the string test, where we "test what we fly".
All three test runs must give the same answer (modulo performance) before a release.
During day-to-day development work, we usually run (2), but occasionally toss in a (1) or (3) just to confirm that we haven't introduced any gaps in test coverage, or bugs that are masked by the debugging logic.
So you are correct that you should not do all your testing using one configuration and then deliver a different build. But that does not mean you can't have extra logic in your code that helps do unit testing and debugging during development and which is excluded from release builds. You just need to make sure that you rerun all tests in the release configuration.
1. The expression to be asserted
2. A set of places (ie variables/references)
3. A format datum and arguments (like sprintf)
When an assert fails you see the formatted message from (3) and get some options:
- invoke a restart to some place higher up the control stack
- invoke the restart generated by the assert which asks for new values for the set of places in step 2, and then retries the assert
- abort
- crash
These options may be invoked by the program itself or by the user if the debugger is invoked
This is interesting - this really reveals their philosophy where they assume that until proven to be good, stuff is considered to be broken.
In safety-protected build modes assertion failures trigger a panic, which is globally overridable, but the default behavior is to print a full stack trace.
Finally, the assertion behavior can be modified at any scope. So one can identify the performance bottlenecks and turn assertion failures into undefined behavior.
#if defined(SQLITE_COVERAGE_TEST) || defined(SQLITE_MUTATION_TEST)
# define ALWAYS(X) (1)
# define NEVER(X) (0)
#elif !defined(NDEBUG)
# define ALWAYS(X) ((X)?1:(assert(0),0))
# define NEVER(X) ((X)?(assert(0),1):0)
#else
# define ALWAYS(X) (X)
# define NEVER(X) (X)
#endif #define ALWAYS(X) (assert(X), (X))
#define NEVER(X) (assert(!(X)), (X))Many people do not know that their compilers optimise away assertions in `-O` ("release") builds.
They use assertions for control flow and input validation, which is very wrong.
I believe that could be trivially fixed by giving functions proper names that indicate what they do or what they are intended for. For example, calling such an optimised-away assertion function `debug_only_assert()` would immediately rule out such unintentional misuse.
The debug vs release build is a bit of a problem still but I think part of the issue is around the idea that debug builds are too slow to use at all or take so long to build anyway so you might as well use release optimizations. This is something to address with faster compilers and better teaching (and perhaps encouraging folks to try learning how to use debuggers effectively).
That is Rust's tack:
> [assert!] assertions are always checked in both debug and release builds, and cannot be disabled.
> Unlike assert!, debug_assert! statements are only enabled in non optimized builds by default. An optimized build will omit all debug_assert! statements unless -C debug-assertions is passed to the compiler.
https://gist.github.com/technion/7c74fad9efd1f14244e25a1cee3...
In sane build systems the command line is visible by default.
Non-sane build systems display wrong percentage numbers instead, but that is a different topic.
Great comment.
Conversely, at a former employer we used a set of 3rd party database libs for ODBC on Linux that for the smallest config error would emit nothing to stderr and call exit(1) from a shared lib. that was beyond annoying
That also has its advantages, but would argue that it is impossible to selectively opt in or out.
Honestly, for a well tested piece of code, the point is moot, since the assert(s) will never evaluate false anyway.
Problem is, you need to understand what you're using, and like it or not, assert is defined as a macro by the C standard. And, assert is conditionally defined (and it's not the only conditionally defined macro).
I know its long, verbose, and dry; but if you want to understand C (or C++ for that matter), you really need to read the ISO standards that correspond to the implantation.
func assert(cond bool){
if !cond {
panic("whoopsies")
}
}Go does have build tags, so one could have 2 files for the assert function, each with opposite build tag logic. It does allow for an effective NOP (it would be a stub function and I believe the compiler would eliminate that).
But this would come with a performance penalty: it makes the function you assert() in no longer a leaf function (it calls assert()) and AFAIK, Go only inlines leaf functions. (So, any function in which you call assert() is no longer eligible for inlining.)
As for assert in Go... couldn't you just make a function that takes a Boolean and panics?
Why do I care about their opinion of Go in SQLite dev docs?
Apparently some peoplr care, considering the number of comments on how Go can, does and should handle this usecase.
Orthogonal: one of my favorite talking points about Erlang is the fact that nearly every line of code is an assertion, and that those assertions are always enabled, even in production.
You’d not want to use Erlang for truly high performance code, granted.
It’s really just a happy byproduct of the overall design of the language and VM. The two primary features that make it happen are immutability and ubiquitous pattern matching.
if(X) {}
else {}
if(Y) {}
else {}
"normal test coverage" is weaker than SQLite tests with the following lines added: testcase(X && Y);
testcase(X && !Y);
testcase(!X && Y);
testcase(!X && !Y);
I wasn't sure when reading the article, to be honest, but making sure you test things you don't explicitly branch on is kinda cool. # define testcase(X) if( X ){ sqlite3Coverage(__LINE__); }
If the testcase(condition) always evaluates to true, the code coverage analyzer would complain that the if ( X ) {sqlite3...} statement never evaluates to false and the branch coverage drops below 100%, the converse holds conversely if it was never evaluated to true in the first place.
Only if the code using the testcase macro gets repeatedly called in a way so that the testcase macro evaluates sometimes to true and sometimes to false, the coverage stays at 100%.sqlite3Coverage() does some dummy work so that the call will not be optimized away by the compiler.
Spicy!
Almost impossible to prove anything in a sufficiently complex system such as SQLite. The difference between assert and always seems arbitrary IMHO