Build table of sin/cos

Is there a way to make this code work? I want to build a table of sin/cos:

#[derive(Clone, Copy, Debug)]
#[repr(C)]
pub struct Rot(f32, f32);

impl Rot {
    const fn new(deg: u16) -> Rot {
        let rad: f32 = (deg as f32) / 180.0 * std::f32::consts::PI;
        let cos = rad.cos();
        let sin = rad.sin();
        Rot(cos, sin)}

    const fn all() -> [Rot; 360] {
        [Self::new(0); 360]}}
1 Like

In the current version of rustc it's not possible to do it with const. If you remove it, it compiles.

If you're committed to this being build time, there's the hack option of generating the table as rust source code in build.rs, then the hilariously overkill option of using a procedural macro. The former is very self explanatory once you know what build.rs is, the latter is rather too much to explain here!

4 Likes

With great surprise I have realized that none of the floating-point functions are const. Moreover, they aren't even unstably const, which is very unexpected. Is it because they delegate to libc, which can set ERRNO?

2 Likes

If I had to guess, because they're affected by CPU flags for things like rounding modes, so it's unclear what the actual value should be?

4 Likes

I would still expect to have some sort of "default sin/cos", which would be usable in const context. Strictly speaking, all calculations of constants are performed by the Rust's const evaluation engine rather than directly by the processor, so it can use whatever semantics it desires. The intrinsics for floating point functions could even evaluate to different functions depending on the context, i.e. have some strict mode of evaluation when called as a const, or lower directly to the libc functions during runtime.

A counterargument would be that it is a footgun if a function have different behaviour depending on the surrounding context. That's true, but I suppose there wouldn't be any significant difference anyway, i.e. it would be on the order of floating point precision. I don't know the FP standard enough to definitely make a case, though.

The issue is that floating point operations are non-deterministic (in particular in respect to the value of NaN payload returned by floating point operations) and currently it's not determined whether const fn should support non-deterministic operations. It's not possible to unstabilize something stabilized, so Rust developers tread carefully here.

5 Likes

Floating point calculations are deterministic. Even ones involving NaN. The issue as I understand it (I asked maybe a month ago) is that the software implementation does not do everything exactly as the hardware does, leading to inconsistent/unexpected results. This isn't because the software is wrong. On the contrary, the hardware returns the wrong value in some situations. There is a fully spec-compliant implementation of floating point arithmetic in rustc, and it is used by MIRI for the unstable methods. There is nothing preventing additional floating point methods from being made unstably const.

2 Likes

After some research, my current understanding of the situation is:

  1. const fns are evaluated at compile time
  2. regular fns are evaluated at runtime
  3. Rust requires that const fns have the same result regardless of if they are evaluated at compile time or runtime
  4. This is hard to guarantee, as arch compiled on can be different from arch code is run on; in fact, the arch the code is run on might not even have a FPU unit
1 Like

Almost. const fns can be evaluated at compile time, but they also can at runtime, if they're given arguments that are determined at runtime

2 Likes

I was thinking...

Depending on the resolution and hence size of your sin/cos lookup tables and depending on what floating point hardware you have available it may well be faster to let the FPU calculate sin/cos for you rather than doing a table look up.

Why? Because tables live in memory, that memory access may well cause cache misses and take 100s or 1000's of times longer than when there is no cache miss.

If performance is the name of the game it might be wise to benchmark this.

2 Likes

Good correction. I think this is precisely the issue: Rust has an expectation that a fn, whether evaluated at compile time or run time, if given the same args, produce the same response. Doesn't sound too unreasonable.

Unfortunately, when the compile arch and runtime arch differ, who knows how f32's behave.

This is true for const fns, yes, they are (currently, and supposed to be) pure. Though, const_heap may also mess with that.

Floats are wack.

To recycle older posts:

TL/DR: The nice monotonic stuff (addition, exp, ...) we can probably make const once we figure out how to deal with NANs. Things where efficient ½ULP accuracy -- and thus portably consistent behaviour -- is fundamentally harder (sin, cos, tan, ...) might never be const, though.

6 Likes