Behavior of floor(), ceiling(), etc

Is there a reason for the unusual behavior of floor(x), ceiling(x), etc., when x is a float type?
I mean the fact that they return another float, not an integer. Unless you explicitly specify Int as the result type.

In mathematics, engineering, etc., these functions are well-known, and defined as producing integers. One therefore has a strong expectation that that’s how they would behave in Julia.
(This unusual behavior caused a bug that cost me a few hours.)

This is not unique to Julia; e.g. C++ has the same behaviour:

Ok, I was not aware of that. But I’m still curious about the rationale, be it for C++ or Julia, as it contradicts common usage.

Would it create some other problem if the behavior were changed just in the case of floats, so as to accord with expectations?

These functions do produce an integer. They just store the integer in a floating-point type by default. Floating-point integers are still integers.

This behavior is pretty universal in programming languages (it is true in C/C++, Python, Go, …), because floating-point values have a much wider range than (hardware) integers. e.g. what result would you want floor(1e100) to give?


Yes, I realize that “floating-point integers” are integers, in the same sense that 5 is a real number. But thanks for the floor(1e100) example, that’s convincing!

1 Like

At this point I think it’s useful to point out that there is a type argument to these functions, so you can do:

julia> floor(Int, 1.5)

Maybe it is also useful to point out floor(value) is much faster than floor(Int, value). Essentially, it is free (measured on intel i7). This could be important for hot loops! Details:

julia> using BenchmarkTools

julia> v = rand(1000000);

julia> f1(v) = ( s = 0.0 ; for value in v s+=floor(value) ; end ; s )
f1 (generic function with 1 method)

julia> f2(v) = ( s = 0 ; for value in v s+=floor(Int,value) ; end ; s )
f2 (generic function with 1 method)

julia> f3(v) = ( s = 0.0 ; for value in v s+=value ; end ; s )
f3 (generic function with 1 method)

julia> f1(v), f2(v), f3(v)   # run once for good luck
(0.0, 0, 500335.41928945674)

julia> @btime f1($v)    # round to Float
  1.040 ms (0 allocations: 0 bytes)

julia> @btime f2($v)    # round to Int
  2.368 ms (0 allocations: 0 bytes)

julia> @btime f3($v)     # do no rounding
  1.037 ms (0 allocations: 0 bytes)

Rough calculation:
Extra-time for Float rounding: 1.040ms - 1.037ms ~ 3ns (free)
Extra-time for Int rounding: 2.368ms - 1.037ms ~ 1331ns (400X)


2x as far as I can see. But nice example.

Compare the times when an “empty” loop is subtracted (i.e. f3(v)). These are the nanoseconds in the calculation at the bottom, and they are >400X.

Ahh, I see what you mean.

But I’m not sure that’s a very useful way of thinking about it. The actual calculation took twice as long.