Contraints on const fns

The big question is whether const fns need to evaluate to exactly the same thing at compile-time and at run-time. You can see this in clang, IIRC, where the folded-by-the-optimizer results for floating-point are actually correct, but don't necessarily match the values you get from your platform's math library at runtime.

Because of the Table-maker's dilemma, fast-and-perfectly-accurate results for floating-point is hard, especially for non-monotonic irrational-period functions like sin. Some things don't have this problem -- like sqrt -- so might be available in const sooner. But some are just hard.

See also https://github.com/rust-lang/rfcs/pull/3352

2 Likes

The main benefit of the perfect (0.5 ulp) accuracy is not so much the extra precision, it's the consistency between platforms and algorithms. There are some smaller benefits too, notably guaranteed monotonicity.

Anything more relaxed than 0.5 ulp (e.g. 1 ulp, or even 0.51 ulp!) is a lot easier to compute in the worst case. That's because to get 0.5 ulp you might need a lot of extra precision bits if you are unlucky, but to get 1 ulp or 0.51 ulp you only need a few extra bits of precision, and you know exactly how many.

One theoretical way to solve the dilemma that I don't see mentioned in the issues is:

  • do perfect 0.5 ulp accuracy for +, -, *, /, sqrt
  • do 1 ulp or 0.51 ulp accuracy for things like log, exp, sin, cos but using the same algorithm everywhere (on all platforms and in const eval)

As far as I can tell, major platforms like x86-64 don't actually do trigonometry in hardware anyway, so it at least seems feasible to have a consistent algorithm. You would still risk the values changing with new Rust versions (if the algorithm gets improved), but maybe that's OK.

1 Like

This is becoming a lot clearer. When I was first looking into this, it seemed like static had similar constraints to const. But now this is compiling just fine, so I think I had another error:
EDIT: this still doesn't work

use std::f64::consts::PI;
static LOG_2PI: f64 = (2.0 * PI).ln();

So, another question: What's the right way to have constants like the above if I don't care about (or really, will have to live without for now) compile-time computation? I could do something like

fn log_2pi() -> f64 {
    (2.0 * PI).ln()
}

But it's weird to have to recompute this value. I could put let LOG_2PI in main and thread it through, but that's pretty awkward. Is hand-coding constants really the only possibility? The ergonomics of that are not great.

LazyCell in std::cell - Rust or
OnceCell in std::cell - Rust

2 Likes

Remember that the optimizer is still allowed to pre-compute it, even if it's not forced to be pre-computed by the rust semantics.

And sure enough, that's exactly what happens: https://rust.godbolt.org/z/YM6EGMYdG

define noundef double @log_2pi() unnamed_addr #0 personality ptr @rust_eh_personality {
  ret double 0x3FFD67F1C864BEB4
}

(LLVM uses hex for complicated floats to be specific about exactly the value that's meant; you can translate it with https://float.exposed/0x3FFD67F1C864BEB4.)

3 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.