Skip to content

ceil/ceilf/floor/floorf seem to do a lot of extra work #846

Open
@Lokathor

Description

@Lokathor

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?

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions