Hacker News new | ask | show | jobs
by raphlinus 2490 days ago
Fun fact: floor(x + 0.5) rounds 0.49999997 to 1.0 (this is 32 bit floats, the same principle applies to 64). Most libraries have slower than ideal round conversion because of historical dross; modern chips have a very fast SIMD round instruction but its behavior doesn't exactly match libc round. See https://github.com/rust-lang/rust/issues/55107 for a deeper discussion.
3 comments

I just tried this on Python3 on a 64-bit x86 system:

    import math
    x = 0.49999999999999994
    print(x-0.5)
    print(math.floor(x+0.5))
I got these printouts:

    -5.551115123125783e-17
    1
So yes, something less than 1/2, with 1/2 added to it, has a floor of 1 in floating point math.

Yet another reminder that floating point calculations are approximations, and not exact.

In Julia it's easy to see the difference between promoting to full number tower and sticking to 64 floats (ed: note difference in result 0 vs 1):

  julia> using Benchmarktools

  julia> @btime floor(0.49999999999999997+0.5)
0.027 ns (0 allocations: 0 bytes)

1.0

  julia> @btime floor(0.49999999999999997+BigFloat(0.5))
280.594 ns (6 allocations: 336 bytes)

0.0

  julia> @code_native floor(0.49999999999999997+BigFloat(0.5))
    .text
  ; ┌ @ floatfuncs.jl:152 within `floor'
    subq    $24, %rsp
    movq    %rsi, 16(%rsp)
    movq    (%rsi), %rax
  ; │┌ @ floatfuncs.jl:152 within `#floor#543'
    movq    %rax, (%rsp)
    movabsq    $jl_system_image_data, %rax
    movq    %rax, 8(%rsp)
    movabsq    $japi1_round_16005, %rax
    movabsq    $jl_system_image_data, %rdi
    movq    %rsp, %rsi
    movl    $2, %edx
    callq    *%rax
  ; │└
    addq    $24, %rsp
    retq
    nopw    %cs:(%rax,%rax)
  ; └

  julia> @code_native floor(0.49999999999999997+0.5)
    .text
  ; ┌ @ floatfuncs.jl:152 within `floor'
  ; │┌ @ floatfuncs.jl:152 within `#floor#543'
  ; ││┌ @ floatfuncs.jl:152 within `round'
    vroundsd    $9, %xmm0, %xmm0, %xmm0
  ; │└└
    retq
    nopw    (%rax,%rax)
  ; └
> Yet another reminder that floating point calculations are approximations, and not exact.

floating-point calculations are absolutely exact. What's not is conversions between decimals and floating-point.

Their not being exact is why 0.49999997 + 0.5 = 1. There isn't enough precision in the range just below 1.0 to represent the number 0.99999997.
> floating-point calculations are absolutely exact

Are you sure about that? What about 1.00000000000001^2 (using eg 64 bit double)?

Maybe the parent wanted to say that the result is detetministic and not far too off from the true value (in fact, as accurate as possible given the format). In the other words FP is exact but abides by a slight different calculation rule involving rounding.
Floating point is usually deterministic, but from a math point of view it is inexact (in most cases it only approximates the exact answer). In many cases that is fine, which is why it is used, but it is important to remember that.
Good call. I usually only use this conversion when the input is approximate to begin with, e.g., feeding a floating point "signal" to a DAC, or computing the contents of a lookup table to be coded into an arduino. To put it another way, I can live with some uncertainty as to the precise threshold going from one output value to the next.
If the number is positive you can substitute floor(x - 0.5) + 1

Or just explicitly check for 0.5 - 1ulp as a special corner case.