Why is Julia not inferring the output type of f(x,y)=x+y?

I’m playing around with type inference in Julia.
I have the following simple function:

f(x,y) = x+y
function q2()
  x = Number[1.0,3]
  a = 4
  b = 2
  c = f(x[1],a)
  d = f(b,c)
  f(d,x[2])
end
@code_warntype q2()

Here’s the output:

MethodInstance for q2()
  from q2() in Main at In[114]:2
Arguments
  #self#::Core.Const(q2)
Locals
  d::Any
  c::Any
  b::Int64
  a::Int64
  x::Vector{Number}
Body::Any
1 ─      (x = Base.getindex(Main.Number, 1.0, 3))
│        (a = 4)
│        (b = 2)
│   %4 = Base.getindex(x, 1)::Number
│        (c = Main.f(%4, a::Core.Const(4)))
│        (d = Main.f(b::Core.Const(2), c))
│   %7 = d::Any
│   %8 = Base.getindex(x, 2)::Number
│   %9 = Main.f(%7, %8)::Any
└──      return %9

I’m surprised that the type of c is Any. Given that f is adding a Float64 and a Int64, I would expect c to be inferred as a Float64.

I can use c::Float64 = f(x[1],a) to get type inference for c and d, which is also surprising b/c if Julia can determine that d is a Float64 by knowing that c is a Float64, then it can track the types that flow through f, but that would imply it could predict the types for c w/o ever having an explicit type annotation.

Even after doing c::Float64, the final line (f(d,x[2])) seems to return Any:

%11 = Main.f(%9, %10)::Any
return %11

This is surprising, b/c Julia knows the type of %9 is Float64 and Julia knows the type of %10 is Number. And presumably Float64 + Number is at the very least Number instead of Any.

Any reason why Julia’s type inference is having a hard time here?

Furthermore, code_warntype displays Number in red. Is it bad if your code has types like Number (instead of more concrete types like Float64)?

Given that f is adding a Float64 and a Int64

%4 = Base.getindex(x, 1)::Number

This is the problem. It doesn’t know that getindex on your Number array returns a Float64.

Replacing the array with a Float64 array, or writing
c = f((x[1])::Float64,a) will also make it type stable.

1 Like

Julia’s type inference algorithm needs to keep compilation time “reasonably small” while also computing precise type information where it “seems likely” that this would result in fast code at runtime.

Fast code machine code can generally be produced when type inference can compute concrete types (or small Unions of concrete types). But when an abstract type like Number is encountered with many subtypes, it’s unlikely to be better to infer Number than to widen the type to Any. So type inference has just chosen to widen the type here, computing a less precise result in favor of lowering compilation time.

By the way, here’s the subtypes of Number defined in Base. There’s also many others in external libraries:

Number
├─ Complex
└─ Real
   ├─ AbstractFloat
   │  ├─ BigFloat
   │  ├─ Float16
   │  ├─ Float32
   │  └─ Float64
   ├─ AbstractIrrational
   │  └─ Irrational
   ├─ Integer
   │  ├─ Bool
   │  ├─ Signed
   │  │  ├─ BigInt
   │  │  ├─ Int128
   │  │  ├─ Int16
   │  │  ├─ Int32
   │  │  ├─ Int64
   │  │  └─ Int8
   │  └─ Unsigned
   │     ├─ UInt128
   │     ├─ UInt16
   │     ├─ UInt32
   │     ├─ UInt64
   │     └─ UInt8
   └─ Rational

In short, yes.

2 Likes

Const prop also doesn’t work through arrays (again, they’re a bit of a black box), but IIRC Shuhei was implementing it for even unryped fields of mutable structs, so it’s conceivable that this will work in the future.

If I explicitly cast the return value to a Float64, like this:

f(x,y) = x+y
function q2()
  x = Number[1.0,3]
  a = 4
  b = 2
  c = f(x[1],a)
  d = f(b,c)
  Float64(f(d,x[2]))
end
@code_warntype q2()

The last 2 lines of the output are:

│   %9  = Main.f(%7, %8)::Any
│   %10 = Main.Float64(%9)::Any
└──       return %10

I’m not sure if I’m reading the notation incorrectly, but it looks like this is saying Main.Float64(%9) returns Any, which seems incorrect since presumably the entire point of Main.Float64 is to either return a Float64 or throw an exception?

struct Foo end
(::Type{Float64})(::Foo) = "not a Float64"

If the Any is a Foo, we’d get a String.

This is legal (but bad), therefore inference is not allowed to inner the return type is Float64.
Use ::Float64 for type asserts on variable declarations or function returns.
These will allow inference to assume the result Float64 (or throw).

It’s generally best to try and avoid the type instability in the first place, thereby avoiding the need to recover from one, if possible.

1 Like

Thanks for the example here.

I’m a little confused by the syntax:

(::Type{Float64})(::Foo) = "not a Float64"

I read: Types · The Julia Language

but I can’t find any mention of this syntax

https://docs.julialang.org/en/v1/manual/types/#man-typet-type

basically x::T means x is a variable and typeof(x) <: T; but what if the argument x you want to pass is a type to begin with? you need x::Type{T}, which would be matched when x === T

Thanks for the explanation!
I think what I’m confused about, is that there is nothing to the left of the :: in ::Type{Float64}.

Or I guess, it looks like we are assigning a string to a variable, but I don’t see any variable name on the left hand side of the assignment.

we’re not using that variable, so we don’t need to give it a name, this is same idea to:

f(x::Vector{<:Any}) = ...
#v.s.
f(x::Vector{T}) where {T<:Any} = ...

if I’m not using T in the method definition’s body, I don’t really care to give it a name

1 Like

Hm, what would the version of this look like where we were assigning to a variable?

More broadly:

  • Seems like ::Type{Float64} is a type
  • I’m not sure how ::Foo (which is also a type?) is interacting with a Float64 given that Foo is a struct. Also not sure how the end result here becomes a string of all things.

this method is dispatched when you have a call that looks like this

a = Foo(...)
Float64(a)

here a::Foo and Float64::Type{Float64}