-
-
Notifications
You must be signed in to change notification settings - Fork 5.7k
Description
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)
falseSimilarly, 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)
falseThis 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)
falseAnd 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)
falseThe 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.0Examples 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))