Typing optional arguments

question

#1

I’m still struggling with (hard) typing and performance; this time consider the following code
it’s not so important what it does (while that also takes some allocations…but not the most for now)

abstract myAbsT
immutable myT <: myAbsT
  v::Float64
end

function myFun{T <: myAbsT, S<: Number}(f::Array{T}, α::Array{S,1};
       MaxIterations::Int64=500, myMask::Array{Bool} = Array(Bool,0)
    )::Array{T}
  iter::Int64 = 1
  if (length(myMask) == 0)
    myMask = falses(f)
  end
  x = f;
  while ( iter < MaxIterations )
    iter = iter+1
    for i in eachindex(x)
      if ~myMask[i]
      x[i] = T(x[i].v+α[1])
      end
    end
  end
  return x
end

preF = ones(2^9,2^9)
@time myFun(myT.(preF), [0.2])
@code_warntype myFun(myT.(preF), [0.2])
@time myFun(myT.(preF), [0.2]; MaxIterations=1000)
@code_warntype myFun(myT.(preF), [0.2]; MaxIterations = 1000)

There’s something really interesting in the warntypes (i happily learned here, they are very helpful).
When running the first @code_warntype one gets
3.809560 seconds (130.57 M allocations: 1.948 GB, 2.27% gc time)
This is still a lot of allocations I don’t know completely where they come from but the body gets shorten tremendously in the profiles. Most interestingly for the second @code_warntype

8.382499 seconds (261.38 M allocations: 3.897 GB, 2.28% gc time)

The number doubles and the time gets quite bad. Looking at the variables, there’s #temp#@_12::Any.
I even started from a larger example and coulnd’t get it smaller than this but still, i can’t find the reason this temporary variable of type any is used and why for this case even the body is not shortened and contains some red types for example the Array{Bool,N} (of course myMask is a bool for every ntry in f but how to fix that?). What’s the reason? I didn’t even touch the mask yet, just set the optional iterations parameter. Coulnd’t figure out what’s happening here. What am I missing here in the typing tricks of Julia?


#2

If I understand your code correctly the reason is that falses(f) produces BitArray not Array{Bool} so the type of variable myMask changes in your function and this causes the allocations.

If you changed the function definition to:

function myFun{T <: myAbsT, S<: Number}(f::Array{T}, α::Array{S,1};
    MaxIterations::Int64=500, myMask::BitArray = falses(f))::Array{T}

then the code works fast.

However, this shows that type declaration for keyword arguments is not enforced in function body (if it was enforced your code would throw an error).


#3

Assuming I’ve understood what you meant (which I’m not sure I have), the declared type of a function argument is never a property of the resulting binding. Code like the following is always fine, so keyword arguments aren’t special in this regard:

function foo(x::Array{Int})
    x = [1.0]
    return x
end

Put another way: the type declaration in the method definition says that this method will be called when the values of the arguments (up to specificity) match the types of the declaration. But within the function’s body, there are no constraints of any sort on the type of the binding for x unless you add those constraints in through another mechanism.


#4

Oh, interesting, i wasn’t even aware that there are two different kind of types for something I would just call boolean.

and @johnmyleswhite thanks for the example and yes I think that’s sadly what I’m doing and of course one should not change type of a variable.

However, interestingly, I’m still left with some internal variable of type Any in the second call and i am not able to find that even If I change the myMask to BitArray


#5

I haven’t found the source of the Any, but one problem is that you change the types of some variables between concrete and non-concrete types.

For example, you have myMask::Array{Bool} = Array(Bool,0), but you really want myMask::Vector{Bool} = Array(Bool,0) if you intend to latter assign myMask = falses(f).

Note that I’m using Vector{Bool} which is an alias for Array{Bool, 1}, whereas your code effectively has Array{Bool, N}. One way to get used to Julia’s types is to make a habit of writing out all type parameters explicitly. This is probably a bad habit in the long run (since there’s no reason you need those parameters in general), but the explicitness can help you understand what you’re declaring versus what the compiler is inferring.


#6

That’s intended, because my data is an array, in the example 2D (ones2^9,2^9) and that should also be possible to use ones(2^8,2^7,4)). And that indeed works fine with BitArray, though the BitArray still is merked in Red due to its unknown dimension N. And I definetly want to have 1,2,3,4,…dimensional data.


#7

You could also introduce array dimension N:

function myFun{T<:myAbsT, S<:Number, N}(f::Array{T,N}, α::Array{S,1};
        maxIterations::Int=1000, myMask::BitArray{N}=falses(f)
    )::Array{T,N}

#8

Thanks Greg,
that’s a neat way to solve the @code_warntypes of the BitArrays. So my code now looks like

function myFun{T <: myAbsT,N}(
      f::Array{T,N}, α::Array{Float64,1};
       MaxIterations::Int64=500,
       myMask::BitArray{N} = falses(f)
      )::Array{T,N}
  iter::Int64 = 1
  x::Array{T,N} = f;
  while ( iter < MaxIterations )
    iter = iter+1
    @fastmath @inbounds for i in eachindex(x)
      if ~myMask[i]
      x[i] = T(x[i].v+α[1])
      end
    end
  end
  return x
end

preF = ones(2^9,2^9)
@time myFun(myT.(preF), [0.2])
@code_warntype myFun(myT.(preF), [0.2])
@time myFun(myT.(preF), [0.2]; MaxIterations=1000)
@code_warntype myFun(myT.(preF), [0.2]; MaxIterations = 1000)

And calling it without the optional argument works fine (though there are still a few allocations; 46 k; and your tips where very helpful to learn using the code warnings. However I’m still stuck with one Any variable and one union. Let me just take some lines from @code_warning

Variables:
  #unused#::#kw##myFun
  #temp#@_2::Array{Any,1}
  ::#myFun
  f::Array{myT,2}
[...]
  #temp#@_11::Int64
  #temp#@_12::Any

Body:
  begin
[...]
      unless (#temp#@_12::Any === :myMask)::Bool goto 20
[...]
      unless (#temp#@_12::Any === :MaxIterations)::Bool goto 24
[....]
      SSAValue(12) = f::Array{myT,2}
      SSAValue(13) = α::Array{Float64,1}
      (Base.throw)($(Expr(:new, :(Base.MethodError), :((Core.getfield)((Core.getfield)((Core.getfield)(#myFun,:name)::TypeName,:mt),:kwsorter)), :((Core.tuple)(#temp#@_2,SSAValue(11),SSAValue(12),SSAValue(13))::Tuple{Array{Any,1},#myFun,Array{myT,2},Array{Float64,1}}))))::Union{}

So somehow this dangling 12th temporary variable is for typechecks of the two optional arguments, am I reading the first two lines I left in the body correctly?
And: Does anybody know there the ::Union comes from? I think (by the two lines before) that might be the x[i] = ... causing that; but why?

Thanks again for all the help, Julia seems somehow a little tricky but fun to find out how it works :slight_smile:


#9

I think you may be hitting issues of keyword functions: keyword-arguments defy inference (there are open issues about it). Consider:

julia> f(x; y::Int=1) = x+y # the ::Int helps some but not all the way
f (generic function with 1 method)

julia> @code_warntype f(5) # it's fine if no key-words are specified
Variables:
  #self#::#f
  x::Int64

Body:
  begin
      return (Base.box)(Int64,(Base.add_int)(x::Int64,1))
  end::Int64

julia> @code_warntype f(5,y=1)
Variables:
  #unused#::#kw##f
  #temp#@_2::Array{Any,1}
  ::#f
  x::Int64
  y::Int64
  #temp#@_6::Int64
  #temp#@_7::Int64
  #temp#@_8::Int64
  #temp#@_9::Any
...

Also benchmarking those two function calls, f(5) takes 1ns whereas f(5,y=1) takes 40ns.

So, moral of it: don’t use keywords in functions which are called in performance critical code.


#10

Very interesting! And I thought it was my fault, because I also narrowed my larger example down to the only thing being different are the optional parameters!
Your short example has the same last _any parameter as mine.

Is there any other way to make them optional? I’d like to use them really as optional ones…even in performance critical code. Of course i could do a wrapper around that, hm.


#11

You can make a convenience keyword-wrapper. Also you can use default positional arguments, those are fine.


#12

Thanks, but i fear i didn’t get the wrapper idea right, doing just ; kwargs...) and making an dictionary out of that, i.e.

      kwargs_dict = Dict(kwargs);
      MaxIterations::Int64=get(kwargs_dict, "MaxIterations", 500)

doesn’t seem to do the trick; it’s still slow it still has the 12th temp ::Any variable. And i don’t want to go into optional positional arguments, because already in this small example i fond’t want to fix the order in which eventually leave them open, so I would like to get to something dictionary like. Without that, some
of my more complicated algorithms would just be unusable.

That said: How can I then use something like keywords in such functions as my example while not loosing performance?


#13

That said: How can I then use something like keywords in such functions as my example while not loosing performance?

Honestly, the real answer is that you don’t. The performance issues with keyword arguments will get fixed one day, but it’s not fixed yet and long-term Julia developers have learned to tolerate this as a weakness of the current implementation of the language.

In this specific example, the use of a dictionary can’t fix things because it’s a total black-box to the type system. You can easily convince yourself of this fact by figuring out why the dictionary can’t possibly know the concrete type of all possible values returned by indexing into the dictionary given arbitrary keys.

One way to handle this is the following:

  • Create a new immutable type that specifies the names and types of the arguments you want to make into keyword arguments.
  • Create a wrapper function that takes in keyword arguments and places those keyword arguments into the immutable type you created before.
  • Pass the immutable type wrapping your keywords to a fast inner function that has clear and enforced types for all arguments.

Of course, you can also avoid the wrapper entirely and just do things by passing the keyword arguments to an inner function. The important thing is that keyword arguments sabotage the type system at present, so you have to use indirection to fix this.


#14

Thank you for the clarification. I think I’ll stick to a wrapper function handling all keyword arguments and calling an inner function having all parameters fixed, so for the above example a function having bot optional arguments as fixed ones, and a wrapper passing onto that function.


#15

This seems like a fine occasion to advertise my Parameters.jl package. It gives you some handy tools for some of the options John described:


using Parameters

function f(x,y;
           a=1, b=2, c=3)
    x+y+a+b+c
end

# using Parameters.jl

@with_kw immutable MT
    a::Int=1
    b::Int=2
    c::Int=3
end

function fw(x,y,
           opts=MT()) # positional-default, constructs MT(1,2,3)
    @unpack a,b,c = opts
    x+y+a+b+c
end

fw(1,1) # =>8

# calling with non-defaults
op = MT(c=99)
fw(1,1,op) # => 104

#16

Thanks mauro for the idea; it looks neat; though I personally like the optional parameters with ;better, but that’s just a personal taste; I wrote a wrapper for that, and this part now seems to work fine.

Interestingly combining this solution here with one from my previous questions yields a further point I can’t get my mind to (still learning all this stuff though). When indexing an neighbor index, as I learned here, i adapted

abstract myAbsT
immutable myT <: myAbsT
  v::Float64
end

function myFun{T <: myAbsT,N}(
      f::Array{T,N}, α::Array{Float64,1},MaxIterations,myMask::BitArray{N}=falses(f))::Array{T,N}
  iter::Int64 = 1
  x::Array{T,N} = f
  R = CartesianRange(size(f))
  while ( iter < MaxIterations )
    iter = iter+1
    for d = 1:ndims(f)
      unitOffset::CartesianIndex{N} = CartesianIndex{N}(ntuple(i::Integer->i::Integer==d ? 1: 0, N))
      for i in R
        i2 = i+unitOffset
        if (i2 in R) && (~myMask[i])
          x[i] = T(x[i2].v+x[i].v)
        end
      end
    end
  end
  return x
end

preF = ones(2^9,2^9)
@time myFun(myT.(preF), [0.2],200)
@code_warntype myFun(myT.(preF), [0.2],200)
@time myFun(myT.(preF), [0.2],200,falses(preF))
@code_warntype myFun(myT.(preF), [0.2],200,falses(preF))

While the first call/warntype works completely fine (maybe because the mask even gets eliminated in optimization), for the second call (well it’s the same values) the d iterate gets Core.Box as a type. Still for this example (not for my larger run on the productive code) both are qually fast and the second oneeven uses only 1.6k allocations while the first requires 39k. Can anyone explain why?

I know I’m asking a lot of questions, for now I’m still trying to understand such details of Julia (coming from Matlab, Mathematica, and such).