Bug in my code from compiler optimization [video](youtube.com)
youtube.com
Bug in my code from compiler optimization [video]
https://www.youtube.com/watch?v=hBjQ3HqCfxs
30 comments
As others pointed out, there also exists `bool::then` that always accepts a closure and doesn't have this issue. In fact both `bool::then` and `bool::then_some` are originated from a single RFC [1], and people unanimously agreed on `bool::then` but not `bool::then_some`, so the latter spun off its own issue [2]. It seems that `bool::then_some` was eventually accepted mainly due to the lack of better alternatives---there were enough people wanting the value form and other pairs of method names were considered better.
[1] https://github.com/rust-lang/rfcs/pull/2757
[2] https://github.com/rust-lang/rust/issues/64260
[1] https://github.com/rust-lang/rfcs/pull/2757
[2] https://github.com/rust-lang/rust/issues/64260
I don't get how changing to if else fixes this. The compiler can still reason that the condition is always going to be true (and if it isn't that is undefined behavior, so f it). And then it will elide the comparison. Same result. The compiler bug here is that for some reason it is not able to apply a valid (lol) optimisation to if else statements. The author should not rely on this. Compiler authors will eventually fix this bug and his game will break again.
> I don't get how changing to if else fixes this. The compiler can still reason that the condition is always going to be true
No, the compiler can't make that assumption for an if statement. That's because the condition of an if statement is always evaluated first, so the following expression(s) are only conditionally evaluated. There's nothing the compiler can do to reason "backwards" in such a case.
then_some effectively flips the order around - the consequent of the "if" statement is evaluated first, and then the condition is evaluated. This reversal of the order makes all the difference.
For a more concrete example, consider the following snippets:
There isn't really a compiler bug here; it's just language semantics coupled with unsafe.
No, the compiler can't make that assumption for an if statement. That's because the condition of an if statement is always evaluated first, so the following expression(s) are only conditionally evaluated. There's nothing the compiler can do to reason "backwards" in such a case.
then_some effectively flips the order around - the consequent of the "if" statement is evaluated first, and then the condition is evaluated. This reversal of the order makes all the difference.
For a more concrete example, consider the following snippets:
if (ptr != nullptr) {
return *ptr;
} else {
return nullptr;
}
This is (probably) safe. The condition is evaluated first, and only if the condition is true is *ptr evaluated. The buggy code, on the other hand, was more like (ptr != nullptr).then_some(*ptr)
In this case, the argument *ptr is evaluated first, before the condition. This is sort of equivalent to: result = *ptr
if (ptr != nullptr) {
return result;
} else {
return nullptr;
}
And that is definitely buggy, as it unconditionally dereferences ptr.There isn't really a compiler bug here; it's just language semantics coupled with unsafe.
> the condition of an if statement is always evaluated first
I am not sure about Rust, but gcc will definitely elide an if check if it detects pointer dereference inside the if block. The reasoning was that if a pointer is being dereferenced, it must be non null. Because if it is null that would be undefined behavior and compiler is allowed to do whatever wants in that case anyway. So
I am not sure about Rust, but gcc will definitely elide an if check if it detects pointer dereference inside the if block. The reasoning was that if a pointer is being dereferenced, it must be non null. Because if it is null that would be undefined behavior and compiler is allowed to do whatever wants in that case anyway. So
if (ptr != NULL) {
val = *ptr
// do something with val
}
becomes // null check removed
val = *ptr
// do something with val
So no, the condition is not guaranteed to be evaluated first, or even evaluated at all. I would be surprised if Rust compiler is not doing these sorts of optimisations.> but gcc will definitely elide an if check if it detects pointer dereference inside the if block
As others have stated, this is almost certainly incorrect.
> The reasoning was that if a pointer is being dereferenced, it must be non null.
The key here is "if a pointer is being dereferenced". In that case the pointer is only dereferenced if the condition evaluates to true and does nothing otherwise. The optimizer would be incorrect to turn that into an unconditional dereference.
As others have stated, this is almost certainly incorrect.
> The reasoning was that if a pointer is being dereferenced, it must be non null.
The key here is "if a pointer is being dereferenced". In that case the pointer is only dereferenced if the condition evaluates to true and does nothing otherwise. The optimizer would be incorrect to turn that into an unconditional dereference.
That's not true.
Only if you dereferenced the pointer before the if-statement.
Only if you dereferenced the pointer before the if-statement.
Source? I think you remember it wrong:
The compiler will only optimize the if away if you already potentially invoked UB. Like this:
The compiler will only optimize the if away if you already potentially invoked UB. Like this:
val = *ptr; // May be *NULL
if (*ptr != NULL) ...In that sense .then_some() is sort of a footgun. Compared to a regular if-statement it invites people to write code in a less efficient (and in this case an incorrect manner). It could just have taken a closure instead.
Edit: it turns out there’s also a .then() that does exactly that.
Edit: it turns out there’s also a .then() that does exactly that.
Not really. `unsafe` is the footgun.
...That sounds like the compiler writer taking undue liberties, and the programmer assuming the machine is capable of reading intent.
Sounds like a great learning experience imo.
Sounds like a great learning experience imo.
Not really. An example that explicitly crashes instead of triggering undefined behavior:
C had a lot of UB that gets thrown away, until some minor shift in optimizations changes it to a crasher. Compare it to an insanely sharp knife: Powerfull, but the slightest error might take off your fingers, it's very hard to know if something is or is not an error, and errors mostly dont cut if your fingers, until surprise surprise they do.
Rust is much better: only unsafe code behaves like C's crazy knife. Only when you use unsafe, the rust guarantees go out of the window, because that's what the programmer explicitly asked.
If n!=0 return 1/n
Is well behaved: Division by zero wont happen. z=1/n
If n!=0 return z
Is not well behaved. It will crash when n=0. What the compiler does is take the existence of z as a proof that n can't be 0, and optimize the if away: z=1/n
return z
Just like division by 0, undefined behaviour (UB) is not allowed. The difference is: UB might crash or give a nonsensical result. You can't tell beforehand. Different versions of a compiler might make different choices of crash vs nonsense. Until recently, the author calculated nonsense, and threw it away, but that was accidental. It might have crashed too.C had a lot of UB that gets thrown away, until some minor shift in optimizations changes it to a crasher. Compare it to an insanely sharp knife: Powerfull, but the slightest error might take off your fingers, it's very hard to know if something is or is not an error, and errors mostly dont cut if your fingers, until surprise surprise they do.
Rust is much better: only unsafe code behaves like C's crazy knife. Only when you use unsafe, the rust guarantees go out of the window, because that's what the programmer explicitly asked.
When you're learning to program, teachers will often tell you "The computer is dumb; it will do exactly what you say, and it can't read your intent." Implicitly, I think most people assume the same of compilers: they're dumb, and will translate an unconditional division as an unconditional division, even when it would crash.
Instead, compilers seemingly try to make insights into the intent or true meaning of your code, often inaccurately, and change it using ad-hoc unintuitive rules as justification (those being the rules of undefined behavior, and what assumptions the authors are and aren't allowed to make about your code). I know they aren't actually reading your intent; it's just the result of many, many passes that each make seemingly reasonable assumptions, which together may add up to a set of unreasonable ones.
> because that's what the programmer explicitly asked.
No, the programmer didn't explicitly ask for their check to be deleted, even with "unsafe" written. There is no request written anywhere. At most, you can say they implied they wanted it by invoking undefined behavior. Probably, though, they just made a mistake, or assumed that behavior was defined when it wasn't (e.g. they may have thought division corresponds to a hardware division instruction, modulo optimizations like right shifting for division by 2, constant folding division by 1, etc.)
Instead, compilers seemingly try to make insights into the intent or true meaning of your code, often inaccurately, and change it using ad-hoc unintuitive rules as justification (those being the rules of undefined behavior, and what assumptions the authors are and aren't allowed to make about your code). I know they aren't actually reading your intent; it's just the result of many, many passes that each make seemingly reasonable assumptions, which together may add up to a set of unreasonable ones.
> because that's what the programmer explicitly asked.
No, the programmer didn't explicitly ask for their check to be deleted, even with "unsafe" written. There is no request written anywhere. At most, you can say they implied they wanted it by invoking undefined behavior. Probably, though, they just made a mistake, or assumed that behavior was defined when it wasn't (e.g. they may have thought division corresponds to a hardware division instruction, modulo optimizations like right shifting for division by 2, constant folding division by 1, etc.)
> No, the programmer didn't explicitly ask for their check to be deleted, even with "unsafe" written. There is no request written anywhere. At most, you can say they implied they wanted it by invoking undefined behavior. Probably, though, they just made a mistake, or assumed that behavior was defined when it wasn't
This is a weird complaint to make here, where the compiler optimization isn't introducing a bug. If the check were left in place, the code would have exactly the same problem that it does without the check.
The optimization is exactly what you hope for: something that makes the code faster without affecting its behavior in any way. Why is that bad?
This is a weird complaint to make here, where the compiler optimization isn't introducing a bug. If the check were left in place, the code would have exactly the same problem that it does without the check.
The optimization is exactly what you hope for: something that makes the code faster without affecting its behavior in any way. Why is that bad?
For some background on why compilers do this, read this gist by ryg. A big issue is we write 32 bit code that runs on 64 bit architectures.
https://gist.github.com/rygorous/e0f055bfb74e3d5f0af20690759...
https://gist.github.com/rygorous/e0f055bfb74e3d5f0af20690759...
This is a very interesting read. Thanks.
[deleted]
You original summary is very poorly phrased. You do mention the problem:
> By calculating y, he accidentally gives a proof to the compiler that x must be true.
But the actual example is written to hide this problem. It should make clear that "y" is actually an expression that depends on x, something like "return x.then_some(x.field)". With that example, it would be immediately clear why evaluating x.field before checking x was a bug.
> By calculating y, he accidentally gives a proof to the compiler that x must be true.
But the actual example is written to hide this problem. It should make clear that "y" is actually an expression that depends on x, something like "return x.then_some(x.field)". With that example, it would be immediately clear why evaluating x.field before checking x was a bug.
You can write this in safe-Rust, with a match. There are only 8 variants, and while it's a bit tedious¹, Rust will optimize it to the same code as if you had done a from_raw construction … without the possibility of UB.²
¹(but there has to be a crate for this)
²https://godbolt.org/z/W55W1GxvY
¹(but there has to be a crate for this)
²https://godbolt.org/z/W55W1GxvY
I wish this wasn't downvoted as for me its the right answer.
unsafe in rust is a blank cheque for the compiler. Doing anything complicated in it is almost guaranteed to have UB somewhere. You decide to disable safety.
If possible, write the safe but tedious code. Then measure. Only when you're sure unsafe is worth it should you take out the big cannon. Write a km of comments proving this code needs and deserves unsafe. Writing the tedious code is less time consuming than debugging the mess.
I wish the video author documented why he choose unsafe here. Was it measured and worth it in an older version of rust? Or a folly of youth (which is fine, as long as you learn from it, been there done that paid the time debugging my stupidity)
unsafe in rust is a blank cheque for the compiler. Doing anything complicated in it is almost guaranteed to have UB somewhere. You decide to disable safety.
If possible, write the safe but tedious code. Then measure. Only when you're sure unsafe is worth it should you take out the big cannon. Write a km of comments proving this code needs and deserves unsafe. Writing the tedious code is less time consuming than debugging the mess.
I wish the video author documented why he choose unsafe here. Was it measured and worth it in an older version of rust? Or a folly of youth (which is fine, as long as you learn from it, been there done that paid the time debugging my stupidity)
>¹(but there has to be a crate for this)
num-traits' FromPrimitive https://docs.rs/num-traits/latest/num_traits/cast/trait.From...
num-traits' FromPrimitive https://docs.rs/num-traits/latest/num_traits/cast/trait.From...
Note that the Rust docs explicitly discourage using `then_some` in the manner described in this video.
https://doc.rust-lang.org/beta/std/primitive.bool.html#metho...
> Arguments passed to then_some are eagerly evaluated; if you are passing the result of a function call, it is recommended to use then, which is lazily evaluated.
https://doc.rust-lang.org/beta/std/primitive.bool.html#metho...
> Arguments passed to then_some are eagerly evaluated; if you are passing the result of a function call, it is recommended to use then, which is lazily evaluated.
The big questions is why clippy (standard linter) didn't catch this. It will complain about expensive expressions in a `.then_some()` because it's eager, why not unsafe?
I believe the parent wants the lint to trigger for any unsafe code in the argument to `then_some`. This would not require any real analysis of the expression.
Clippy would only complain about expensive expressions when it can prove that the alternative would be always better than the original (for example, `or_fun_call` [1]). It is not impossible to measure the cost of given expression to suggest that, but that would increase a risk of false positives. The OP has the same challenge---this kind of range analysis can only occur (and currently does occur) much later than Clippy.
[1] https://rust-lang.github.io/rust-clippy/master/index.html#/o...
[1] https://rust-lang.github.io/rust-clippy/master/index.html#/o...
You want miri, not clippy. Clippy is for minor issues in code, miri is for finding UB in Rust.
In testing I want miri, but knowing that there already is a very similar lint, I want that expanded
He wants code:
With y an unsafe expression, and x always true if y can be evaluated without invoking undefined behaviour.
He wrote it as:
This always calculates y, but throws it away if x is false.
By calculating y, he accidentaly gives a proof to the compiler that x must be true. The proof is of course invalid, but as y is unsafe, the compiler trusts him nevertheless. So the 'if' gets optimized away.