Conversion and type stability


#1

I am writing a code in which the input file is a Julia script, and the user may provide some input in the form of functions (of one or several variables).

The code may use these functions by calling them with Float64, or with other datatypes. I have automatic differentiation in mind, but for this MWE, let us say it’s Int64 vs. Float64.

Now, most input functions would use all their inputs
f = (x,y) -> x^2+y
but the user may say “Oh the input parameters do not really matter”
g = (x,y) -> 1

This causes me two types of problems

a) If I call an input function with two Float64, two Int64, or with an Int64 and a Float64. I need to rely on the output being promote_type(typeof(x),typeof(y)), that is, Float64.

Without countermeasures, calling g would wreak havoc in my dispatching logic further down the line. That issue, I can solve:

promote_eltype(v...)   = promote_type(eltype.(v)...)
convert_eltype(a,v...) = convert(promote_eltype(a,v...),a)
typestable(f,x...)     = convert_eltype(f(x...),x...)::promote_eltype(x)
F(x...)                = typestable(g,x...)
G(x...)                = typestable(f,x...)

So if I use F and G in place of f and g, my software does the computations I want.

b) But it does it slowly. Behold:

> @code_warntype typestable(f,3,2.)

Variables:
  #self#::#typestable
  f::##49#50
  x::Tuple{Int64,Float64}

Body:
  begin 
      SSAValue(0) = (Core.getfield)(x::Tuple{Int64,Float64}, 1)::Int64
      return (Base.add_float)((Base.sitofp)(Float64, (Base.mul_int)(SSAValue(0), SSAValue(0))::Int64)::Float64, (Core.getfield)(x::Tuple{Int64,Float64}, 2)::Float64)::Float64
  end::Float64

> @code_warntype F(3,2.)

Variables:
  #self#::#F
  x::Tuple{Int64,Float64}

Body:
  begin 
      SSAValue(1) = Main.g
      SSAValue(3) = (Core.getfield)(x::Tuple{Int64,Float64}, 1)::Int64
      SSAValue(4) = (Core.getfield)(x::Tuple{Int64,Float64}, 2)::Float64
      $(Expr(:inbounds, false))
      # meta: location untitled-33e486cf08c224e1e95c30ab1d254fb4 typestable 8
      SSAValue(2) = (SSAValue(1))(SSAValue(3), SSAValue(4))::ANY
      # meta: pop location
      $(Expr(:inbounds, :pop))
      return (Core.typeassert)((Main.convert_eltype)(SSAValue(2), SSAValue(3), SSAValue(4))::ANY, $(QuoteNode(Real)))::REAL
  end::REAL

So while calling “typestable” gives me, well, typestable code, any attempt at a syntactic sugar wrap makes me lose type stability.

I can live without the syntactic sugar, but if the implication is that typestability is not “exported by a function” then I am in CPU-trouble, and worse, I do not understand the logic of it all.

What is going on here, why are F and G not typestable, when they return a value that is stamped for type?


#2

global nonconst variables are bad, mkay.

const f = (x,y) -> x^2+y
const g = (x,y) -> 1
julia> @code_warntype F(3,2.)
Variables:
  #self# <optimized out>
  x <optimized out>

Body:
  begin
      return (Base.sitofp)(Float64, $(QuoteNode(1)))::Float64
  end::Float64

You could also just define non anonymous functions like:

f(x,y) = x^2 + y

Note that your F calls g and G calls g, probably just a typo.


#3

Thanks, as usual, great help from you.

F = …g… not a problem in the MWE, but would look bad in the software!!!:grinning:

I am trying the get the logic: if f is a non-const closure, I can define F, and then redefine f, and that keeps the compiler very cautious. If f is a const closure, I’d have to recompile to redefine it.

But if f is a function-not-closure f(x,y) = x^2+y then I can still redefine it. And this affects the behaviour of F, but F is typestable. Hmm… why?


#4

f is not a closure here, it is just a non const binding to the anonymous function (x,y) -> x^2 + y. And since you are accessing f from F you are using a non const global binding.

Because Julia has special functionality for dealing with the case of redefining functions. If you redefine the function f, what the compiler (basically) will do is to recompile everything that depends on the result of f. And since everything is recompiled with the new f in mind, then it is type stable again.


#5

Non-const binding, vs. redefining-is-recompiling a function. Got it! Thanks a million!

This const business got me trice. Once with non-const global variables, then non-const type aliases, and now, non-const binding to an anonymous function! When will I learn…