It’s not about expensiveness, it’s about side effects. An expensive operation can still be deleted. Back to the original example, the first thing it does is lock a mutex - an operation which has side effects.
Gotcha, that makes sense. Appreciate the explanation.
I thought like if I put the function call directly in the assert, the compiler might optimize away the whole thing in release builds since asserts can be compiled out. By separating it, we guarantee the function call actually happens regardless of what the compiler does with the assertion.
I was has always assumed the same thing as you. I was never sure, nor took the time to test it, but simply coded defensively around it. I am confident that my code has multiple instances of superfluous temporary variables that I don’t need just for an assert:
const actual = foo();
assert(actual == expected_foo_result);
Instead of just a simple:
assert(foo() == expected_foo_result);
…just because I was not sure about if foo()
would be optimized away in release builds…and too lazy to ever test it.
I am actually happy about this revelation, makes assert
more ergonomic than I have been giving it credit for.
This is completely wrong. Please take some time to wrap your head around unreachable
- it is essential that all Zig programmers understand exactly what it means and doesn’t mean.
You’re right, I was confused about how unreachable works. I thought it could eliminate the entire expression evaluation, but I see now that it only affects the failure branch. The function call itself still has to execute to evaluate the condition. Thanks for the correction.
No problem, and as you can see you’re not alone - looks like 2 other people in this thread thought the same thing.
I didn’t find this before posting, you might find it interesting as well https://github.com/ziglang/zig/issues/10942
I’ll ask a follow-up question so I can point to this in the future:
What specifically is considered side-effects here - is it currently whatever the LLVM optimizer pipelines decides? Does that include mutating memory, or just syscalls/extern calls as indicated in the issue?
This is completely unrelated to LLVM. Side effects are 100% well-defined in the Zig language.
Examples of Not side effects:
- calling a function
- loading from memory
- storing to memory
- inline assembly
- atomics
unreachable
in unsafe optimization modes- everything not in the following list
Complete, exhaustive list of all side effects:
- loading through a
volatile
pointer - storing through a
volatile
pointer - inline assembly with
volatile
keyword - atomics with
volatile
pointers - calling an extern function
@panic
,@trap
,@breakpoint
unreachable
in safe optimization modes (equivalent to@panic
)
Well that’s a relief, TIL
While this makes total sense in a logical way. It is completely different to how many other programming languages does it. The fact that you need to write a list on the conditions for outcome A vs outcome B should be a warning sign that will generate a ton of issues in the future, just to be right and clever.
I know this isn’t a democracy but I vote -1 on this.
What other programming languages are you thinking of in particular that do it differently?
From the top of my head I know that C/c++/c#/python all throw away the entire assertion expression. Making it significant if you evaluate your expression inside the assert or not.
The problem with letting the compiler decide is that if you add analytics measurements to your code, it always have side effects. I’m in favor of giving the programmer the power to choose instead of making a magic decision for him.
I see that you are only talking about how assert
works, and not about side effects in general. In C/C++, side effects work basically the same as I have described them above for Zig.
The difference with regards to assert has to do with the presence or absence of preprocessor macros. In the case of Python it seems they special case assert into being a language primitive.
In zig, there is no preprocessor, and there is no special case for assert. You seem to be interpreting a lack of magic as the existence of magic, which I find difficult to take seriously.
Heh, tbh std.debug.assert being evaluated in release-fast mode is also something I didn’t expect and didn’t know until now (nor did I bother to check), guess those C roots run deep
I’ll need to go through my home computer emulator code and re-evaluate the use of asserts, because there’s a ton of them in the hot path…
This makes technical sense after explanation, but I feel like this could be a bit of a footgun. Not a fault of the language itself, but simply because people aren’t coming to Zig out of a vacuum, but usually with experience in other languages where this may seem like atypical behavior.
As others have said, and I don’t want to belabor the point, but the foreknowledge of how other languages simply throw away the assert statement (e.g. C) in combination with assert
’s documentation, I hope to illustrate that it is easy to make the incorrect assumption.
The docs state that they are kept for debug builds, and “optimized away” in release builds, which could easily be construed as the whole statement, not just the assert itself when read through the perspective of someone who already expects that the entire assert statement will be removed.
If nothing else, perhaps it might be worth clarification in the documentation that the inner statement does not get optimized away, even if it may feel superfluous to someone with deeper understanding of the language. I have mistakenly been sprinkling asserts all throughout my code, believing them to be free in release builds, regardless of if it were a hot path or not.
/// In Debug and ReleaseSafe modes, calls to this function are always
/// generated, and the `unreachable` statement triggers a panic.
///
/// In ReleaseFast and ReleaseSmall modes, calls to this function are optimized
/// away, and in fact the optimizer is able to use the assertion in its
/// heuristics.
While your logic takes the upper hand, your strict adherence to logic will cause alot of pain and frustration.
Even if my argument is flawed it’s not without merit. Please take that seriously.
The idea of an assertion is that you’re debug checking something for correct usage. That check could be expensive, it could require temporary allocations, it could log something or it could call functions that are analytics measured.
It’s fine that you don’t want a magic function that breaks with normal zig logic, I can respect that, but that begs an @assert keyword.
For analytics is there a way to tell the compiler that even though a function has side effects it should not save it’s caller (and there by itself) from being pruned?
But you know that already because it’s a normal function.
I don’t disagree, and that does make sense, now that it was pointed out.
This is kind of missing the forest for the trees here. A very common expectation of an assert function is that it simply goes away, everything, just poof, consider the line deleted. So one day you pick up Zig and start learning it, and you read the docs, it “confirms” what you think you already know, that the it gets optimized away. Great, fantastic, exactly what you thought it should do. Except it doesn’t. It optimizes away the actual assert call only (except when it doesn’t, which is not documented either, but I digress).
This isn’t about technical knowledge of the inter workings of the language, it is the deviation of a common norm in CS that I suggest is worth mention in documentation. This thread alone has multiple people, some with a respectable amount of Zig experience, who were also incorrect in our assumptions about this minor detail, which could be solved with a single sentence doc-comment.
This is quite different from Java too (at least the versions I used to use). asserts aren’t even enabled without a command line option, and its nice to know the whole line vanishes. But makes me wonder about ‘if’ statement shortcutting, a similar situation. eg in Java we’d do if (lowCostBool && highCost()) to avoid the expensive call if the bool check failed. Is that handled then in the same way as asserts, everything evaluated?