Type instability for singleton tuple argument

why do I get type instability for the function (x->x.^2).((1,))? if my input argument is a singleton tuple, then @code_warntype (x->x.^2).((1,)) gives

│   %4 = Base.materialize(%3)::Tuple{Any}
└──      return %4

If the argument is not a singleton tuple, there doesn’t seem to be any type instability.

I also noticed that after fiddling with things in the REPL for a while, I no longer get the type warning. However, I see the type warning again after restarting the REPL.

Not seeing that on Julia 1.5:

julia> @code_warntype  (x->x.^2).((1,))
Variables
  #self#::Core.Compiler.Const(var"##dotfunction#253#8"(), false)
  x1::Tuple{Int64}
  #7::var"#7#9"

Body::Tuple{Int64}
1 ─      (#7 = %new(Main.:(var"#7#9")))
│   %2 = #7::Core.Compiler.Const(var"#7#9"(), false)
│   %3 = Base.broadcasted(%2, x1)::Base.Broadcast.Broadcasted{Base.Broadcast.Style{Tuple},Nothing,var"#7#9",Tuple{Tuple{Int64}}}
│   %4 = Base.materialize(%3)::Tuple{Int64}
└──      return %4
1 Like

Odd. I’m running it on v1.5 too, and I get

@code_warntype (x->x.^2).((1,))
Variables
  #self#::Core.Compiler.Const(var"##dotfunction#253#16"(), false)
  x1::Tuple{Int64}
  #15::var"#15#17"

Body::Tuple{Any}
1 ─      (#15 = %new(Main.:(var"#15#17")))
│   %2 = #15::Core.Compiler.Const(var"#15#17"(), false)
│   %3 = Base.broadcasted(%2, x1)::Base.Broadcast.Broadcasted{Base.Broadcast.Style{Tuple},Nothing,var"#15#17",Tuple{Tuple{Int64}}}
│   %4 = Base.materialize(%3)::Tuple{Any}
└──      return %4

I had to restart the REPL to see it. Does the warning show up if you do that?

I got an answer on slack from @simeonschaub.

The problem here seems to be that inlining gives up for x -> x.^2, probably because it has to go through the whole broadcast machinery twice. This fixes it: @code_warntype (x->(Base.@_inline_meta; x.^2)).((1,)).

If you don’t need all the broadcasting semantics, you’re usually better off just using map instead, e.g. map(x->x.^2, (1,)) also infers just fine.

The latter map implementation is exactly what I was looking for.

I think I was under the wrong impression that broadcasting and map were somewhat equivalent.

1 Like

I think the problem you are running into here is that the machinery to make broadcasting work puts quite a bit of strain on the compiler and because you are trying to broadcast twice here, it seems to fail to inline x -> x.^2. You can encourage inlining manually like this:

julia> @code_warntype (@inline x -> x.^2).((1,))
Variables
  #self#::Core.Compiler.Const(var"##dotfunction#275#23"(), false)
  x1::Tuple{Int64}
  #22::var"#22#24"

Body::Tuple{Int64}
1 ─      (#22 = %new(Main.:(var"#22#24")))
│   %2 = #22::Core.Compiler.Const(var"#22#24"(), false)
│   %3 = Base.broadcasted(%2, x1)::Base.Broadcast.Broadcasted{Base.Broadcast.Style{Tuple},Nothing,var"#22#24",Tuple{Tuple{Int64}}}
│   %4 = Base.materialize(%3)::Tuple{Int64}
└──      return %4

(note that on Julia versions older than 1.5, you need to write that as (x -> (@_inline_meta; x.^2)).((1,)))

A more general workaround for code like this is to rely less on broadcasting and prefer functions like map, which make inference’s job a lot easier. For example, this also infers just fine:

julia> @code_warntype map(x->x.^2, (1,))
Variables
  #self#::Core.Compiler.Const(map, false)
  f::Core.Compiler.Const(var"#15#16"(), false)
  t::Tuple{Int64}

Body::Tuple{Int64}
1 ─ %1 = Base.getindex(t, 1)::Int64
│   %2 = (f)(%1)::Int64
│   %3 = Core.tuple(%2)::Tuple{Int64}
└──      return %3

map still does the same thing here, but its implementation is a lot less complicated than broadcast.

2 Likes