You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Must a const fn behave exactly the same at runtime as at compile-time? #77745
Depending on the platform's NaN behavior, the result will differ between runtime and compile-time execution of foo(1.0, 0.0). Compile-time execution is determined by the Rust port of apfloat (a soft-float implementation); runtime behavior depends on the actual NaN patterns used by the hardware which are not always fully determined by the IEEE specification.
Note that this is entirely independent of any optimizations; we are discussing here the relationship between code that the user explicitly requests to be executed at compile-time, and regular run-time code. Optimizations apply to all code equally and they treat fn and const fn the same, so the the questions of how floating-point operations can be optimized is an entirely separate from and off-topic for this issue.
I think you are asking two different questions here and treat them as if they are the same. Let me explain. :)
More concretely, when we optimize the following code, are we allowed to const propagate the foo call?
Note that this is a different question from the issue title. The behavior at runtime could be non-deterministic according to the spec, so even if you see one particular runtime behavior and a different compile-time behavior, it would still be correct to do const-propagation. Floating-point operations likely are non-deterministic.
I feel rather strongly that const fn must behave in a way that is allowed to occur at runtime; that is just a different way of saying that CTFE must implement the Rust spec. It would be rather strange if that was not the case. From this alone it already follows that const propagation like you are asking is allowed. I cannot see any reasonable way (assuming a bug-free CTFE engine) in which this optimization is not allowed.
But then there is the separate question, do we want to allow non-deterministic operations in const fn? CTFE inherently has to make some choice to resolve the non-determinism(*), and that choice might be different from what codegen+LLVM happen to currently do, which could be surprising for programmers that do not expect such non-determinism to actually be observable (even though it could, in theory, be observable even without any CTFE being involved). I think this is really the question you are asking here, but it is unrelated to const propagation. Unlike "is this optimization correct", this question cannot be answered by proving a theorem; this is a judgment call we could make either way as part of language design.
(*) Actually that is not entirely true -- allocation base addresses are another example of non-determinism, and there CTFE uses a form of symbolic execution to basically track all possible non-deterministic choices at once, and halt evaluation for cases where that is not possible. This is required because the non-deterministic choice made for a certain allocation must be consistent for a given Rust program across const-time and run-time execution. For runtime code that choice is only made when the program actually starts (thanks to ASLR) and thus CTFE has to be done in a way that is compatible with every possible choice made later. We do not need such a heavy hammer for floating-point operations because their non-deterministic choice is much more local, confined to each individual operation.
I wonder in what real-world use case would somebody want to do complex calculations with NaN-s in compile time. I also wonder if the answer is the same if a compile-time calculation results in a NaN - is it something somebody actually wants, or does it hide an error?
If NaN is the only concern, then the const fn implementation could just error out every time a NaN is encountered/obtained from computation. I also worry about floating point implementations having minuscle differences even for well-defined floats, but can't come up with an example. I see miri gives an error when a dangling pointer is returned in a constant as well (requires nightly to trigger it), so it's doable.
Reacted by Solomon Ucko, Evgeniy Dushistov, Jesse Ruderman, Lily, Trevor Gross, A248 and Vladas Zakrevskis
So.. if we have a const F: f32 = A + B; and at runtime a let f: f32 = A + black_box(B);, then it is not necessary for F.to_bits() == f.to_bits(), but it must at least be possible for that to be equal for some execution of the runtime code (even if not observable in practice due to CPU bugs).
I also worry about floating point implementations having minuscle differences even for well-defined floats, but can't come up with an example.
I always thought that even non-NaN floating point math can differ (in miniscule ways) between hardware even if it's the same target triple.
the const fn implementation could just error out every time a NaN is encountered/obtained from computation
That's one way, but it may be expensive. We specifically do not validate in const eval like we do in miri, because that validation is expensive. Doing it for floats may be cheaper, but if we also have to look at floats in large structs or arrays it can get expensive very quickly again. We could of course just check during each operation, which is much more direct and should only impact float ops. I think that if nondeterministic operations include non-NaN numbers, then that doesn't help us a lot though.
I think that we can just make a judgement call on how we make such nondetermism deterministic (by choosing one possibility), even if that choice changes between target platforms, compiler versions, optimization levels or other compiler flags.
The main question is then, how to make that choice I guess. Just put it on the const eval roadmap that I should really really finish and have T-lang sign it off?
So.. if we have a const F: f32 = A + B; and at runtime a let f: f32 = A + black_box(B);, then it is not necessary for F.to_bits() == f.to_bits(), but it must at least be possible for that to be equal for some execution of the runtime code (even if not observable in practice due to CPU bugs).
No CPU bugs involved. If the spec says that non-deterministically, A or B can happen, then it is completely okay for runtime execution to always do A. Or to do A on Tuesdays and B every other day of the week. Or to do A only in crates whose name starts with a vowel. Or whatever. And CTFE can make its own choice of A or B completely independently on that.
So in your case, both f.to_bits() and F.to_bits() must be results that are allowed by the spec, but since the spec might allow multiple results, the two do not have to be the same. And that's really all we can say; there is no requirement that running the program many times must eventually produce all possible results or so. (This distinguishes non-determinism from randomness.)
I always thought that even non-NaN floating point math can differ (in miniscule ways) between hardware even if it's the same target triple.
AFAIK IEEE fully specifies what happens for primitive FP operations (except for NaN bits). Basically the operation has to return the best possible approximation to the result of the computation if it were carried out on actual rational numbers. (Transcendental functions are a different game.) The remaining differences here really are down to CPU bugs -- 32bit x86 is notorious here, and I hear some architectures handle subnormals (values very close to 0) incorrectly.
That's one way, but it may be expensive. We specifically do not validate in const eval like we do in miri, because that validation is expensive. Doing it for floats may be cheaper, but if we also have to look at floats in large structs or arrays it can get expensive very quickly again. We could of course just check during each operation, which is much more direct and should only impact float ops.
If the latest analysis in #73328 is correct, then only floating-point operations are non-deterministic, but copying around FP values is deterministic. I do not think it would be expensive to check at each floating point addition etc whether the result is deterministic (basically, if the result is non-NaN, but we can easily add further conditions), and raise an error if it is not. This has nothing to do with checking the validity invariant.
I think that if nondeterministic operations include non-NaN numbers, then that doesn't help us a lot though.
Why that? I think it would work just as well. Unless of course all operations are non-deterministic, then this would be equivalent to ruling out FP entirely.
Why that? I think it would work just as well. Unless of course all operations are non-deterministic, then this would be equivalent to ruling out FP entirely.
What I mean is if we error whenever any operation returns a NaN, but still have other nondeterministic ops, then we haven't gained anything and should either keep ruling out FP ops or just allowing NaN and not checking anything.
If the latest analysis in #73328 is correct, then only floating-point operations are non-deterministic, but copying around FP values is deterministic.
Oooh, neat. That is an improvement to the info I had when we created min_const_fn
What I mean is if we error whenever any operation returns a NaN, but still have other nondeterministic ops, then we haven't gained anything and should either keep ruling out FP ops or just allowing NaN and not checking anything.
Sure, we'd have to capture every possible form of non-determinism. Also everything I say on this topic should be checked by an FP expert, which I am not.^^
Oooh, neat. That is an improvement to the info I had when we created min_const_fn
Yes, new info came up since then. Basically I think we should just copy whatever WebAssembly does -- it has an exhaustive and precise spec, and I am sure they involved enough FP experts to make sure the spec is also realistically implementable. Also this means if LLVM does soemthing else we can complain that they are incompatible with WebAssembly, which gives our complaint more weight. ;)
Reacted by Zach Lym, Zicklag, nicoo, Lily and Iris
Transcendental functions (e.g. trigonometry and non-integral exponentiation), can give different results between platforms. I don't think we can take a "hard-line" approach, since then functions like f32::tan that will take advantage of hardware support if it exists can never be const fn; Users would have to opt-in to a software emulated version (either in std or in the ecosystem). I view CTFE as just another platform on which floating point is supported. Users don't (or at least shouldn't) expect cross-platform consistency at runtime, why is CTFE any different?
Even trying to promise that the result given by CTFE is consistent between compiler versions would be foolish, since it would lock us into a particular software emulation strategy that may become outdated. I think we should guarantee that CTFE engine for a given compiler at a given optimization level is deterministic between runs and nothing more. I think we might already have this for NaN payloads, although it would be nice to have an actual set of rules for how they get handled instead of "whatever LLVM does".
If NaN is the only concern, then the const fn implementation could just error out every time a NaN is encountered/obtained from computation. I also worry about floating point implementations having minuscle differences even for well-defined floats, but can't come up with an example.
-@est31
An example of what you may be thinking about is, for instance, ARMv7 Neon (the SIMD vector unit) flushing subnormal float values to zero. A floating-point operation in a register that flushes subnormal values to zero would potentially cause an alteration of the value even when the mantissa is not supposed to change (e.g. f32x4::abs). This is no longer a concern on ARMv8 Neon (usually "aarch64"), which will not do that if you don't ask it to, as far as I'm aware (and I am reasonably sure I would have found out by now).
Part of the reason that x86_64 is well-behaved while x86 is not is that, as @thomcc told me, on x86_64 the XMM registers (introduced as SIMD registers) are fully compliant and compilers exhibit a preference for them even for "scalar" floating point ops. That may sound like a niche oddity but I have been finding in my reading and experiments that "The floating point registers and SIMD registers are actually the same" is actually a fairly common hardware implementation approach... correct FP math is expensive in transistors to implement, so I suppose one saves some surface area on the die by not doing so twice.
pubfnf32_add(a:f32,b:f32) -> f32{
a + b
}pubfnf32_abs(a:f32) -> f32{
a.abs()}
; Compiled with rustc -C opt-level=3 -Cdebuginfo=0 -Ccodegen-units=1 --target armv7-unknown-linux-gnueabihfexample::f32_add: vadd.f32 s0, s0, s1bx lrexample::f32_abs: vabs.f32 s0, s0bx lr
Yes, those are vectorinstructions. I had developed a slightly more thorough platform comparison for x86 and Arm targets at the link (nothing particularly exciting, mind) but I excerpted that because it does seem that at least on some ARMv7 triples there is a similar default to using the (potentially buggy, as mentioned) vector unit for floating point math... a sound choice elsewhere, an implementation concern here.
There's more extensive documentation on all the nuances of Arm's various FP implementations spread across Arm's website but I found a relatively succinct explanation of some of the details on Debian's wiki in relation to their own support choices: https://wiki.debian.org/ArmHardFloatPort#Background_information
That will still mean, however, that we give up on const fn being deterministic functions at runtime. There are some situations where it'd be really nice to say "this function must be pure" and have the compiler check that (DerefPure anyone?). I am not sure if it was ever realistic or a good idea to use const fn for this purpose, but it seems at least possible in principle.
AFAIK IIRC const functions were the rationale to reject adding a remove the pure keyword, as const was the only tractable definition of purity. Edit: a lot of people were expecting to use const functions for specifying contracts and formal methods work.
At the risk of spamming this thread, couldn't you use the WASM interpretation by default but give programmers the option of matching runtime behavior by whitelisting deterministic functions like augmentedMath and (I think) trigPi? I'm a big fan of Watt and would love to see WASM leveraged for more compile time operations. I'll take this to the chat.
I am going to use this space to talk about const_eval_select because it has no tracking issue. Basically, I'm not fond of the safety requirement for const_eval_select.
Here's an example where safe code might want to use it: it seems desirable to have some kind of debug_or_const_assert! in certain cases -- in fact, stdlib already uses this pattern all over the place to check for soundness violations.
Unfortunately, currently this is only useful for checking unsafe code, despite it being pretty reasonable to have the result of violating the condition to be "incorrect behavior" rather than "unsoundness" (if it's useful I can come up with several concrete places I'd want to use this).
It also seems to create some weird incentives to make/keep invariants as safety invariants (rather than correctness invariants). For example, let's imagine that say I have an unsafe const fn that checks it a safety invariant in this manner (that is, this is using the hypothetical debug_or_const_assert! implemented using const_eval_select).
If it turns out that after some time, the initial reason for making the function unsafe is no longer valid (this is common in my experience, as rustc improves we need less unsafe to get good performance), I still can't make the function safe unless I either give up performing the check at compile time, or accept doing it at runtime too (which may not be acceptable). This is a big bummer, and encourages keeping this function unsafe.
And all that is ignoring how easy it seems to violate the requirement, especially if they only differ in some edge case that the author may not have considered, either because one of them has a bug, or because they use some platform intrinsic that is only mostly identical to the operation the const code performs (due to the author not reading the documentation precisely enough, perhaps).
To be clear: having them differ semantically would certainly result in confusing and undesirable behavior, and I'm not saying it should be encouraged -- just the opposite! What I am saying is that we have a lot of precedent in std for not considering it to be unsound for users to do confusing/undesirable things, especially in the case where they're likely to do so inadvertently.
In other words, I feel that differing const/runtime behavior with const_eval_select feels... kinda similar to having a buggy Ord impl -- it's confusing and undesirable, but also will inevitably occur by accident.
I'll also note that this part of the documentation...
Safe code in other crates may assume that $reasonable_assumption_that_isnt_always_true
... is true for all sorts of things (including the implementation of traits like Ord). This isn't a problem, since bugs happen. And then the example goes on to show a case where unsafe code makes this the assumption.
It seems totally reasonable to me that it's fine to push the safety requirement onto the code that contains unreachable_unchecked() -- it's the thing that needs the assumption to hold for soundness.
This would mean that unsafe code can't rely on this being true for external code, similar to how unsafe code shouldn't assume traits in third-party crates are correctly implemented.
Reacted by Lokathor, Jonas Platte, Ben Kimock and nora
The current requirements on const_eval_select reflect an abundance of caution. I am not aware of there being anything fundamentally wrong with having functions that behave differently at compile-time and run-time, and allowing such differences would also bring float-in-const a lot closer to stabilization. However, that should be done very explicitly, not as a byproduct of introducing an intrinsic that was needed for a quick hack. That's why we went with the super paranoid version for now.
Would you like to draft an RFC to officially declare that we are okay with the same function behaving differently on the same input in runtime and const contexts? I think that is the kind of official process we need. (Possibly a T-lang FCP could also be enough, but I don't know what the FCP would be on, so an RFC seems better.)
Another question that the RFC probably needs to settle at the same time: must a const fn, when called at runtime in a way that it could also have been called at compile-time, always behave deterministically and without side-effects (other than writing to references explicitly given to the function)?
This is currently true, and it is potentially useful for unsafe code: given a const fn, unsafe code might never actually want to call this function in const context, but it might want to do things which are only correct when that function is deterministic and side-effect-free. But overall I feel that the costs of this restriction (which effectively tries to use const as an effect system for ensuring some form of purity) outweigh the benefits. Still, abolishing that rule should be done explicitly and with intent, not accidentally.
Would you like to draft an RFC to officially declare that we are okay with the same function behaving differently on the same input in runtime and const contexts...
Uh, hm. Sure, I guess I have some time for this. I likely will need someone more familiar with const eval to actually look over it after I have something, do you mind?
(This may be in a bit since I have actually promised another RFC at rustconf)
Re: const fn purity, in general I'm not a fan of telling unsafe code they can rely on the behavior of things like this. IMO you should be pretty careful when unsafe and only trust your code, and code that has made a promise to uphold the behavior that you need (possibly an implicit promise in the case of std, for example).
In particular, while it's not supported for this to happen (and may even be UB), things like the floating point environment may change externally, which could influence the results. If Rust code is in a cdylib, this could happen externally across two separate calls. So, promising that unsafe code may rely on this sounds a little negligent -- it's not a promise we can actually keep in all cases. (While float env changes might be a form of UB, they definitely will happen, and the things unsafe code could get up to if it believes this is impossible might be worse (exploitable) UB -- who knows).
Also this sounds like it would end up limiting the functionality that can be provided by const fn in the future, which could end up being a bummer.
(Also, people who want purity should consider what that actually looks like, and try to get it added to the language. Tacking it on to "what constant evaluation functions do at runtime" is not the way to go about it IMO)
Uh, hm. Sure, I guess I have some time for this. I likely will need someone more familiar with const eval to actually look over it after I have something, do you mind?
Yeah just ping @rust-lang/wg-const-eval and we'll be happy to give feedback. :) AFAIK we all want something like this to happen (@oli-obk@lcnr stop me if I am wrong).
While float env changes might be a form of UB, they definitely will happen
They must happen in a way that Rust code never sees it, i.e., changing it back to the previous configuration before control flows back to Rust. Then it's fine. Otherwise it's UB and you might have bigger problems.
Also this sounds like it would end up limiting the functionality that can be provided by const fn in the future, which could end up being a bummer.
Yes indeed that is my main reason for wanting to drop this restriction. I am not sure how const-heap support will look like but creating a heap allocation is not pure...
(Also, people who want purity should consider what that actually looks like, and try to get it added to the language. Tacking it on to "what constant evaluation functions do at runtime" is not the way to go about it IMO)
I was thinking about this and the only way this issue can ever be addressed is if Rust had a software floatng point implementation that perfectly emulates each supported platform (not only nan shenanigans but also x87 weirdness etc). Then it would work even if cross compiling
On one hand this would introduce a great burden to every supported platform, to the point that many platforms would need to be marked as "const fn differs from fn". On the other hand, it's probably possible to borrow code from projects like qemu or emulators in general, so it seems at least doable.
I'm interested in writing an RFC for allowing this (assuming no one else is working on one already). I'll reach out on Zulip once I have the first draft for feedback (feel free to reach out to me before that if you want to tell me something important about it) 🙂
Reacted by Oli Scherer and Aaron Bies
Reacted by Thom Chiovoloni, Ralf Jung and Aaron Bies
Disallow constant floating-point f32 operations, through the standard library built-in a soft floating-point SoftF32, and provide constant conversion between the two?
The user performs the calculation in a constant context by converting the floating point to a soft floating point, thus sensing possible platform differences.
Is there already a third-party repository for this?
Reacted by Jiahao XU, Oleksandr Babak and Asger Hautop Drewsen
With rust-lang/rfcs#3514 having been accepted, part of the answer here is given: it is okay for a const fn to return different answers for the same input and compiletime and runtime, at least as long as both invocations could have given the same answer (but the non-deterministic choice ended up being different).
What is still open is whether we want a safe stable way of accessing const_eval_select, but that's tracked at #124625.
Activity
RalfJung commentedon Oct 9, 2020
(Replying to an older version of the OP)
I think you are asking two different questions here and treat them as if they are the same. Let me explain. :)
Note that this is a different question from the issue title. The behavior at runtime could be non-deterministic according to the spec, so even if you see one particular runtime behavior and a different compile-time behavior, it would still be correct to do const-propagation. Floating-point operations likely are non-deterministic.
I feel rather strongly that
const fnmust behave in a way that is allowed to occur at runtime; that is just a different way of saying that CTFE must implement the Rust spec. It would be rather strange if that was not the case. From this alone it already follows that const propagation like you are asking is allowed. I cannot see any reasonable way (assuming a bug-free CTFE engine) in which this optimization is not allowed.But then there is the separate question, do we want to allow non-deterministic operations in
const fn? CTFE inherently has to make some choice to resolve the non-determinism(*), and that choice might be different from what codegen+LLVM happen to currently do, which could be surprising for programmers that do not expect such non-determinism to actually be observable (even though it could, in theory, be observable even without any CTFE being involved). I think this is really the question you are asking here, but it is unrelated to const propagation. Unlike "is this optimization correct", this question cannot be answered by proving a theorem; this is a judgment call we could make either way as part of language design.(*) Actually that is not entirely true -- allocation base addresses are another example of non-determinism, and there CTFE uses a form of symbolic execution to basically track all possible non-deterministic choices at once, and halt evaluation for cases where that is not possible. This is required because the non-deterministic choice made for a certain allocation must be consistent for a given Rust program across const-time and run-time execution. For runtime code that choice is only made when the program actually starts (thanks to ASLR) and thus CTFE has to be done in a way that is compatible with every possible choice made later. We do not need such a heavy hammer for floating-point operations because their non-deterministic choice is much more local, confined to each individual operation.
bugadani commentedon Oct 9, 2020
I wonder in what real-world use case would somebody want to do complex calculations with NaN-s in compile time. I also wonder if the answer is the same if a compile-time calculation results in a NaN - is it something somebody actually wants, or does it hide an error?
est31 commentedon Oct 9, 2020
If NaN is the only concern, then the const fn implementation could just error out every time a NaN is encountered/obtained from computation. I also worry about floating point implementations having minuscle differences even for well-defined floats, but can't come up with an example. I see miri gives an error when a dangling pointer is returned in a constant as well (requires nightly to trigger it), so it's doable.
oli-obk commentedon Oct 9, 2020
So.. if we have a
const F: f32 = A + B;and at runtime alet f: f32 = A + black_box(B);, then it is not necessary forF.to_bits() == f.to_bits(), but it must at least be possible for that to be equal for some execution of the runtime code (even if not observable in practice due to CPU bugs).I always thought that even non-NaN floating point math can differ (in miniscule ways) between hardware even if it's the same target triple.
That's one way, but it may be expensive. We specifically do not validate in const eval like we do in miri, because that validation is expensive. Doing it for floats may be cheaper, but if we also have to look at floats in large structs or arrays it can get expensive very quickly again. We could of course just check during each operation, which is much more direct and should only impact float ops. I think that if nondeterministic operations include non-NaN numbers, then that doesn't help us a lot though.
I think that we can just make a judgement call on how we make such nondetermism deterministic (by choosing one possibility), even if that choice changes between target platforms, compiler versions, optimization levels or other compiler flags.
The main question is then, how to make that choice I guess. Just put it on the const eval roadmap that I should really really finish and have T-lang sign it off?
RalfJung commentedon Oct 9, 2020
No CPU bugs involved. If the spec says that non-deterministically, A or B can happen, then it is completely okay for runtime execution to always do A. Or to do A on Tuesdays and B every other day of the week. Or to do A only in crates whose name starts with a vowel. Or whatever. And CTFE can make its own choice of A or B completely independently on that.
So in your case, both
f.to_bits()andF.to_bits()must be results that are allowed by the spec, but since the spec might allow multiple results, the two do not have to be the same. And that's really all we can say; there is no requirement that running the program many times must eventually produce all possible results or so. (This distinguishes non-determinism from randomness.)AFAIK IEEE fully specifies what happens for primitive FP operations (except for NaN bits). Basically the operation has to return the best possible approximation to the result of the computation if it were carried out on actual rational numbers. (Transcendental functions are a different game.) The remaining differences here really are down to CPU bugs -- 32bit x86 is notorious here, and I hear some architectures handle subnormals (values very close to 0) incorrectly.
See rust-lang/unsafe-code-guidelines#237 for some more open questions about "what even are our floating-point semantics".
If the latest analysis in #73328 is correct, then only floating-point operations are non-deterministic, but copying around FP values is deterministic. I do not think it would be expensive to check at each floating point addition etc whether the result is deterministic (basically, if the result is non-NaN, but we can easily add further conditions), and raise an error if it is not. This has nothing to do with checking the validity invariant.
Why that? I think it would work just as well. Unless of course all operations are non-deterministic, then this would be equivalent to ruling out FP entirely.
oli-obk commentedon Oct 9, 2020
What I mean is if we error whenever any operation returns a
NaN, but still have other nondeterministic ops, then we haven't gained anything and should either keep ruling out FP ops or just allowingNaNand not checking anything.Oooh, neat. That is an improvement to the info I had when we created
min_const_fnRalfJung commentedon Oct 9, 2020
Sure, we'd have to capture every possible form of non-determinism. Also everything I say on this topic should be checked by an FP expert, which I am not.^^
Yes, new info came up since then. Basically I think we should just copy whatever WebAssembly does -- it has an exhaustive and precise spec, and I am sure they involved enough FP experts to make sure the spec is also realistically implementable. Also this means if LLVM does soemthing else we can complain that they are incompatible with WebAssembly, which gives our complaint more weight. ;)
ecstatic-morse commentedon Oct 9, 2020
Transcendental functions (e.g. trigonometry and non-integral exponentiation), can give different results between platforms. I don't think we can take a "hard-line" approach, since then functions like
f32::tanthat will take advantage of hardware support if it exists can never beconst fn; Users would have to opt-in to a software emulated version (either instdor in the ecosystem). I view CTFE as just another platform on which floating point is supported. Users don't (or at least shouldn't) expect cross-platform consistency at runtime, why is CTFE any different?Even trying to promise that the result given by CTFE is consistent between compiler versions would be foolish, since it would lock us into a particular software emulation strategy that may become outdated. I think we should guarantee that CTFE engine for a given compiler at a given optimization level is deterministic between runs and nothing more. I think we might already have this for NaN payloads, although it would be nice to have an actual set of rules for how they get handled instead of "whatever LLVM does".
workingjubilee commentedon Oct 9, 2020
An example of what you may be thinking about is, for instance, ARMv7 Neon (the SIMD vector unit) flushing subnormal float values to zero. A floating-point operation in a register that flushes subnormal values to zero would potentially cause an alteration of the value even when the mantissa is not supposed to change (e.g.
f32x4::abs). This is no longer a concern on ARMv8 Neon (usually "aarch64"), which will not do that if you don't ask it to, as far as I'm aware (and I am reasonably sure I would have found out by now).Part of the reason that x86_64 is well-behaved while x86 is not is that, as @thomcc told me, on x86_64 the XMM registers (introduced as SIMD registers) are fully compliant and compilers exhibit a preference for them even for "scalar" floating point ops. That may sound like a niche oddity but I have been finding in my reading and experiments that "The floating point registers and SIMD registers are actually the same" is actually a fairly common hardware implementation approach... correct FP math is expensive in transistors to implement, so I suppose one saves some surface area on the die by not doing so twice.
In fact, this got me curious enough to feed some code into Godbolt...
Yes, those are vector instructions. I had developed a slightly more thorough platform comparison for x86 and Arm targets at the link (nothing particularly exciting, mind) but I excerpted that because it does seem that at least on some ARMv7 triples there is a similar default to using the (potentially buggy, as mentioned) vector unit for floating point math... a sound choice elsewhere, an implementation concern here.
There's more extensive documentation on all the nuances of Arm's various FP implementations spread across Arm's website but I found a relatively succinct explanation of some of the details on Debian's wiki in relation to their own support choices: https://wiki.debian.org/ArmHardFloatPort#Background_information
tavianator commentedon Oct 10, 2020
This GCC bug is possibly related to this discussion: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=93681. LLVM may have similar issues, I'm not sure.
The moral is that it's important that compile-time float evaluation can't lead the optimizer to make assumptions that may be contradicted at runtime.
61 remaining items
indolering commentedon Jun 10, 2021
AFAIKIIRCconstfunctions were the rationale toreject adding aremove thepurekeyword, as const was the only tractable definition of purity. Edit: a lot of people were expecting to useconstfunctions for specifying contracts and formal methods work.At the risk of spamming this thread, couldn't you use the WASM interpretation by default but give programmers the option of matching runtime behavior by whitelisting deterministic functions likeI'll take this to the chat.augmentedMathand (I think)trigPi? I'm a big fan of Watt and would love to see WASM leveraged for more compile time operations.memoryruins commentedon Jun 10, 2021
@indolering
purewas a keyword before and duringconst fn, and was unreserved as a keyword ~2 years ago with the rationale in https://github.com/rust-lang/rfcs/blob/master/text/2421-unreservations-2018.md#rationale-for-pure (but this is unrelated to this issue so let's leave it at that)thomcc commentedon Aug 5, 2022
I am going to use this space to talk about
const_eval_selectbecause it has no tracking issue. Basically, I'm not fond of the safety requirement forconst_eval_select.Here's an example where safe code might want to use it: it seems desirable to have some kind of
debug_or_const_assert!in certain cases -- in fact, stdlib already uses this pattern all over the place to check for soundness violations.Unfortunately, currently this is only useful for checking unsafe code, despite it being pretty reasonable to have the result of violating the condition to be "incorrect behavior" rather than "unsoundness" (if it's useful I can come up with several concrete places I'd want to use this).
It also seems to create some weird incentives to make/keep invariants as safety invariants (rather than correctness invariants). For example, let's imagine that say I have an
unsafe const fnthat checks it a safety invariant in this manner (that is, this is using the hypotheticaldebug_or_const_assert!implemented usingconst_eval_select).If it turns out that after some time, the initial reason for making the function unsafe is no longer valid (this is common in my experience, as
rustcimproves we need lessunsafeto get good performance), I still can't make the function safe unless I either give up performing the check at compile time, or accept doing it at runtime too (which may not be acceptable). This is a big bummer, and encourages keeping this function unsafe.And all that is ignoring how easy it seems to violate the requirement, especially if they only differ in some edge case that the author may not have considered, either because one of them has a bug, or because they use some platform intrinsic that is only mostly identical to the operation the const code performs (due to the author not reading the documentation precisely enough, perhaps).
To be clear: having them differ semantically would certainly result in confusing and undesirable behavior, and I'm not saying it should be encouraged -- just the opposite! What I am saying is that we have a lot of precedent in
stdfor not considering it to be unsound for users to do confusing/undesirable things, especially in the case where they're likely to do so inadvertently.In other words, I feel that differing const/runtime behavior with
const_eval_selectfeels... kinda similar to having a buggyOrdimpl -- it's confusing and undesirable, but also will inevitably occur by accident.I'll also note that this part of the documentation...
... is true for all sorts of things (including the implementation of traits like
Ord). This isn't a problem, since bugs happen. And then the example goes on to show a case where unsafe code makes this the assumption.It seems totally reasonable to me that it's fine to push the safety requirement onto the code that contains
unreachable_unchecked()-- it's the thing that needs the assumption to hold for soundness.This would mean that
unsafecode can't rely on this being true for external code, similar to how unsafe code shouldn't assume traits in third-party crates are correctly implemented.RalfJung commentedon Aug 6, 2022
The current requirements on
const_eval_selectreflect an abundance of caution. I am not aware of there being anything fundamentally wrong with having functions that behave differently at compile-time and run-time, and allowing such differences would also bring float-in-const a lot closer to stabilization. However, that should be done very explicitly, not as a byproduct of introducing an intrinsic that was needed for a quick hack. That's why we went with the super paranoid version for now.Would you like to draft an RFC to officially declare that we are okay with the same function behaving differently on the same input in runtime and const contexts? I think that is the kind of official process we need. (Possibly a T-lang FCP could also be enough, but I don't know what the FCP would be on, so an RFC seems better.)
Lokathor commentedon Aug 6, 2022
For something of this magnitude the additional publicity of an RFC would be essential.
RalfJung commentedon Aug 6, 2022
Another question that the RFC probably needs to settle at the same time: must a
const fn, when called at runtime in a way that it could also have been called at compile-time, always behave deterministically and without side-effects (other than writing to references explicitly given to the function)?This is currently true, and it is potentially useful for unsafe code: given a
const fn, unsafe code might never actually want to call this function inconstcontext, but it might want to do things which are only correct when that function is deterministic and side-effect-free. But overall I feel that the costs of this restriction (which effectively tries to useconstas an effect system for ensuring some form of purity) outweigh the benefits. Still, abolishing that rule should be done explicitly and with intent, not accidentally.thomcc commentedon Aug 7, 2022
Uh, hm. Sure, I guess I have some time for this. I likely will need someone more familiar with const eval to actually look over it after I have something, do you mind?
(This may be in a bit since I have actually promised another RFC at rustconf)
Re: const fn purity, in general I'm not a fan of telling unsafe code they can rely on the behavior of things like this. IMO you should be pretty careful when unsafe and only trust your code, and code that has made a promise to uphold the behavior that you need (possibly an implicit promise in the case of std, for example).
In particular, while it's not supported for this to happen (and may even be UB), things like the floating point environment may change externally, which could influence the results. If Rust code is in a cdylib, this could happen externally across two separate calls. So, promising that unsafe code may rely on this sounds a little negligent -- it's not a promise we can actually keep in all cases. (While float env changes might be a form of UB, they definitely will happen, and the things unsafe code could get up to if it believes this is impossible might be worse (exploitable) UB -- who knows).
Also this sounds like it would end up limiting the functionality that can be provided by
const fnin the future, which could end up being a bummer.(Also, people who want purity should consider what that actually looks like, and try to get it added to the language. Tacking it on to "what constant evaluation functions do at runtime" is not the way to go about it IMO)
RalfJung commentedon Aug 7, 2022
Yeah just ping @rust-lang/wg-const-eval and we'll be happy to give feedback. :) AFAIK we all want something like this to happen (@oli-obk @lcnr stop me if I am wrong).
They must happen in a way that Rust code never sees it, i.e., changing it back to the previous configuration before control flows back to Rust. Then it's fine. Otherwise it's UB and you might have bigger problems.
Yes indeed that is my main reason for wanting to drop this restriction. I am not sure how const-heap support will look like but creating a heap allocation is not pure...
👍
dlight commentedon Aug 7, 2022
I was thinking about this and the only way this issue can ever be addressed is if Rust had a software floatng point implementation that perfectly emulates each supported platform (not only nan shenanigans but also x87 weirdness etc). Then it would work even if cross compiling
On one hand this would introduce a great burden to every supported platform, to the point that many platforms would need to be marked as "const fn differs from fn". On the other hand, it's probably possible to borrow code from projects like qemu or emulators in general, so it seems at least doable.
RalfJung commentedon Aug 7, 2022
Not allowing float operations in
const fnalso "addresses" the problem. ;) Not in a way that makes anyone happy, though...Note that there has been tons of prior discussion on this specific problem, also considering e.g. disallowing NaNs in
const.Noratrieb commentedon Oct 13, 2022
I'm interested in writing an RFC for allowing this (assuming no one else is working on one already). I'll reach out on Zulip once I have the first draft for feedback (feel free to reach out to me before that if you want to tell me something important about it) 🙂
823984418 commentedon Feb 7, 2023
I have an idea:
Disallow constant floating-point
f32operations, through the standard library built-in a soft floating-pointSoftF32, and provide constant conversion between the two?The user performs the calculation in a constant context by converting the floating point to a soft floating point, thus sensing possible platform differences.
Is there already a third-party repository for this?
823984418 commentedon Feb 7, 2023
I simply copied the soft floating point implementation in
compiler_builtins:const_soft_float
Is it possible?
RalfJung commentedon Aug 4, 2024
With rust-lang/rfcs#3514 having been accepted, part of the answer here is given: it is okay for a
const fnto return different answers for the same input and compiletime and runtime, at least as long as both invocations could have given the same answer (but the non-deterministic choice ended up being different).What is still open is whether we want a safe stable way of accessing const_eval_select, but that's tracked at #124625.