Skip to content

Incorrect results from floating-point division (fld, cld, div) #49450

@yurivish

Description

@yurivish

Julia has a div function that is used to implement floor division (fld) and ceil division (cld). I think I’ve found a bug that causes all of these division functions to return incorrect results.

Floor division is documented to return the largest integer less than or equal to x / y. It should never return a number that is greater than x / y.

But it does:

julia> 514 / Float16(0.75)
Float16(685.5)

julia> div(514, Float16(0.75)) # should round down, but rounds up instead
Float16(686.0)

julia> fld(514, Float16(0.75)) # likewise
Float16(686.0)

julia> fld(514, Float16(0.75))  514 / Float16(0.75)
false

Similarly, ceil division should never return a number that is smaller than regular division, but it does:

julia> 515 / Float16(0.75)
Float16(686.5)

julia> cld(515, Float16(0.75)) # should round up, but rounds down instead
Float16(686.0)

julia> cld(515, Float16(0.75))  515 / Float16(0.75)
false

This behavior is not limited to 16-bit floats. Here’s a case where fld produces an incorrect result for Float32 inputs:

julia> 4_194_307 / Float32(0.75) # = 5592409.5
5.5924095f6

julia> fld(4_194_307, Float32(0.75)) # = 5592410, incorrectly rounded up
5.59241f6

julia> fld(4_194_307, Float32(0.75))  4_194_307 / Float32(0.75)
false

And here’s the same for cld:

julia> 4_194_308 / Float32(0.75) # = 5592410.5

julia> cld(4_194_308, Float32(0.75)) # = 5592410, incorrectly rounded down
5.59241f6

julia> cld(4_194_308, Float32(0.75))  4_194_308 / Float32(0.75)
false

The equivalent operations in Python produce the correct results:

# For 16-bit floats:
>>> np.float16(514) / np.float16(0.75) # regular division
685.5
>>> np.float16(514) // np.float16(0.75) # floor division
685.0

# For 32-bit floats:
>>> np.float32(4_194_307) / np.float32(0.75) # regular division
5592409.5
>>> np.float32(4_194_307) // np.float32(0.75) # floor division
5592409.0

Examples of this incorrect behavior are not hard to find – for most floats, you can find a divisor that will make either fld or cld return the wrong answer.

Here are some examples for Float16 where either fld or cld is incorrect:

  • cld(1, Float16(0.000999)) < 1 / Float16(0.000999)
  • cld(2, Float16(0.001999)) < 2 / Float16(0.001999)
  • cld(3, Float16(0.002934)) < 3 / Float16(0.002934)
  • cld(4, Float16(0.003998)) < 4 / Float16(0.003998)
  • fld(5, Float16(0.004925)) > 5 / Float16(0.004925)

And here are some for Float32:

  • fld(5, Float32(6.556511e-7)) > 5 / Float32(6.556511e-7)
  • fld(10, Float32(1.3113022e-6)) > 10 / Float32(1.3113022e-6)
  • fld(11, Float32(1.4305115e-6)) > 11 / Float32(1.4305115e-6)
  • cld(16, Float32(2.8014183e-6)) < 16 / Float32(2.8014183e-6)
  • cld(17, Float32(2.2053719e-6)) < 17 / Float32(2.2053719e-6)

For simplicity I’ve presented examples where the first argument is an integer; this bug also occurs for non-integral inputs.

A divisor producing the wrong result can be found for over 51% of all possible 16-bit floats. I have not evaluated how widespread this is for Float32, but the results above suggest that it is similarly easy to find failures there too.

I’ve tracked the invocations down to this definition of div, which has existed at least as far back as Julia 1.6:

div(x::T, y::T, r::RoundingMode) where {T<:AbstractFloat} =
	convert(T, round((x - rem(x, y, r)) / y))

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugIndicates an unexpected problem or unintended behaviorcorrectness bug ⚠Bugs that are likely to lead to incorrect results in user code without throwingfloat16mathsMathematical functions

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions