Description
So I had a look at the ceiling and floor operations recently, and I was eventually pointed to the magical f32
constant 8388608.0_f32
. This is the smallest f32
that can't have a fractional part. In other words, 8388607.5
is the biggest f32
that has a fractional part, and you can have f32
values greater than that but they'll always be whole number values. The next bit pattern is 8388608
(no fractional bits active). If we step 1 bit higher we have an active fractional bit, but the value is 8388609
, still a whole number.
The source of this magical constant is that you want the exponent part to be 2^[mantissa bits stored], so for f32
you want the exponent part to be 2^23. The same concept holds with f64
, you just have an exponent part of 2^52: 4503599627370496.0_f64
This means that we can have a ceilf
function for f32
that's really simple:
pub fn ceilf(f: f32) -> f32 {
if absf(f).to_bits() < 8_388_608.0_f32.to_bits() {
let truncated = f as i32 as f32;
if truncated < f {
truncated + 1.0
} else {
truncated
}
} else {
f
}
}
This will pass the test assert_eq!(ceilf(val), val.ceil())
for all possible 32-bit patterns a float can take. I haven't done a test with the f64
version for all possible 64-bit patterns of course, but no value tested so far has shown a different result than the stdlib result.
The current libm implementation of ceilf is, well, a lot more steps than that. Similarly, ceil
, floor
, and floorf
are all doing quite a bit of work.
Is there some sort of spec that the current functions are trying to match with? Or should we consider converting to this simpler style?