Float constants

There are some inconsistencies in the constants in Float. There is f32::PI_2, Float::two_pi, and Float::frac_pi_2. I find it hard to come up with consistent names for all the constants, but there may be a better solution: why have all those fractions at all? frac_pi_2 is only one character shorter than (pi / 2.0). (pi * 2.0) is four characters more than two_pi, but you don’t always need the parentheses. I propose that the following constants be removed from Float:

  • sqrt2
  • frac_1_sqrt2
  • two_pi
  • frac_pi_2
  • frac_pi_3
  • frac_pi_4
  • frac_pi_6
  • frac_pi_8
  • frac_1_pi
  • frac_2_pi
  • frac_2_sqrtpi
  • log2_e
  • log10_e
  • ln_2
  • ln_10

The only math constants to keep are pi and e.

For multiples and fractions, it should not matter from a performance point of view due to constant folding, right? If we keep e.g. two_pi, people will write pi * 2.0 anyway, which leads to inconsistencies. (I did a search on GitHub, and I indeed found cases like this. It might have been pi * 0.5 instead of the fraction.)

I conducted a GitHub search a few weeks ago, with the following results:

  • frac_pi_2 is used in 16 places, excluding the Rust project and wrappers.
  • frac_pi_6 is not used, except in wrappers.
  • frac_2_sqrtpi is not used, except in wrappers.
  • pi has 1985 search results. I did not count how many were wrappers, but for the other constants that was around 35, so there probably are more than 1900 places where pi is used.

For the precomputed function values, these can be computed manually with little effort. There is a runtime cost involved here (please correct me if I am wrong, I do not know much about the compiler internals), but you can do the pre-computation manually, or you can put the constant in your own code if you really need a constant. If we do allow precomputed function values, then the decision which ones to include is arbitrary. sqrt2 is common, but why not sqrt3 and sqrt5 then? I think that in a situation where you need a very specific constant, you are likely to need other constants (not in Float) as well, so you’d need to define the missing ones anyway.

When I searched Rust files on GitHub for sqrt2, I found seven results that were not wrappers:

  • return 0.5 * (1.0 + unsafe { erf(x / Float::sqrt2()) });
  • let sqrt2 = 2.0f32.sqrt();, out[sh_index(l, m)] = sqrt2 * x * y * z;
  • let sqrt2: T = Float::sqrt2();, (x / sqrt2, z / sqrt2)
  • z *= 1f64 / SQRT2; (uses the constant from f64)
  • let p1 = Particle {m: 5., pos: Vector2{x: -SQRT2/0.2, y: -SQRT2/0.2} }; (uses the constant from f64)
  • v.xyz[ix] *= SQRT2; (uses the constant from f32)
  • let sqrt2 = (2.0 as f32).sqrt();

People compute or define it manually anyway, and division by sqrt2 is used instead of multiplication by frac_1_sqrt2. I found one use of sqrt3 (which had to be defined manually).

Conclusion: removing exotic constants would allow the names of the remaining constants to be consistent. Many constants appear to be (virtually) unused, and if you do need them, defining them manually is no big deal.

The constants are there because they have higher precision than manually calculated ones if I remember correctly.

2 Likes

It’s worth pointing out that the current constants are at least partly included to match those in math.h

@bjz may be able to comment further?

We could possibly simplify things here.

println!("{}", f32::consts::FRAC_PI_2 == (f32::consts::PI / 2.0));

gives true

which ones are the higher precision ones?

The way floating point numbers work, any power of two times a constant still has the same accuracy. 1/2 == 2^-1 is a power of two, so this doesn’t really surprise me. Have you checked frac_pi_3, frac_1_pi?

FRAC_PI_3 is still true, FRAC_1_PI is false, BUT:

println!("{}", f32::consts::FRAC_1_PI == (1.0 / f64::consts::PI).to_f32().unwrap());

this is still true also, saving it to a variable works

let x = 1.0 / f32::consts::PI;
println!("{}", f32::consts::FRAC_1_PI == x);

this changes it to true

I tested all of the members, for f32 and for f64. For f32, the imprecise ones are FRAC_2_SQRTPI and LOG10_E. For f64, the imprecise ones are FRAC_1_SQRT2, FRAC_PI_3 and FRAC_PI_6. I assigned everything to a variable first.

I also compared the computed values and the constants. For f64::consts::FRAC_1_SQRT2, only the least significant bit differed. For the other two f64 constants, the two least significant bits differed. For f32, both differences were only the least significant bit.

That’s why you need f128 so you can compute the values in 128 bits and downcast to 64 bits.

@ruuda that suggests we shouldn’t remove seemingly redundant math constants in favor of functions

FYI: http://www.wolframalpha.com/input/?i=1%2Fpi+to+base+16

@nodakai: It also suggests that most of the current constants are redundant. There are still reasons for removing even the constants with more precision:

  • The constants are not used extensively, as the GitHub search suggests. I think part of the problem is discoverability; you expect pi to be there, but when you need 2/sqrt(pi), do you go and check the docs whether there is a constant for that?
  • The decision which constants to include is arbitrary. Why does pi/8 deserve a constant, but pi/7 not?

If you do care about the one extra bit, defining the constants manually is just one extra line of code. Also, @iopq, that does work for f32 currently: you can compute as f64 and then downcast. This enables all current f32 constants to be reconstructed perfectly.

There is also the issue of runtime cost for computing values on the fly. But there will always be constants that are not in the standard library (e.g. pi/7). If you need such a constant, you must construct it manually, either in terms of other constants and functions, or pre-computed. You can make the performance/accuracy/convenience decision there. If you really need that one bit of extra precision, or if you really need it to be constant, define it manually.

There are advantages to having constants, but it is impossible to have all constants that people will ever use in the standard library. I think there is no good objective criterion to decide whether a constant should be in the standard library or not. Not including any but the most basic constants is a way to avoid arbitrariness.

As I understood it the criterion was very objective so far: Include all those that are also available in C's math.h.

two_pi, frac_pi_3, frac_pi_6 and frac_pi_8 are not in math.h.

Maybe we could add untyped constants à la Go; that is, constants that are merely interpolated into code that uses them. So const PI = 3.141592653589793238462643383279 would declare a constant that could be either an f32 or an f64, and could keep its precision when multiplied by a constant. This would also be useful if we ever got custom literals: PI could be inferred to be a Decimal or Rational type, for example, without the need to define a separate new constant.

@ruuda I agree with you in that std::f32::consts is redundant when we have std::f64::consts. Anyone can simply do let x: f32 = std::f64::consts::FRAC_2_SQRTPI as f32.

However trait methods have more flexibility in terms of generic programming. If someone wants to write a generic function fn greater_than_pi<T: Float + PartialOrd>(x: T) -> bool like this, only having the Pi constant of type f64 is not nice. Generic programming will allow him/her to extend his/her code with external crates providing f128 types or interval arithmetic at any time.

As for 2/sqrt(pi), anyone writing a numerical code would first check for it because 1/sqrt(pi) is an important normalization factor for Gaussian function exp(-x*x). I don’t know what the numerator 2 is for, though…

Replacing two_pi() and frac_pi_8() with 2 * pi() and pi() / 8 also needs some consideration from the viewpoint of generic programming because we need a lengthy code like std::num::from_int::<T>(2i).unwrap() as of now, IIUC

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.