Avoiding functions in types; performance comparison & invitation for code review


#1

Dear All,

I’m attempting to go full Julia and not store functions in a type. The meat of my problem requires doing a some quadrature using QuadGK.jl on the product of some bessel functions using SpecialFunctions.jl and a function or it’s derivative. I’m always able to take the derivative by hand so I don’t rely on automatic differentiation. Please see this gist which contains a MWE with enough complexity to represent the broader problem.

The gist contains four files

  • Main.jl; to run the code do julia Main.jl from e.g. the terminal
  • Work.jl; where the calculations are actually performed
  • Objects.jl; contains my OOP approach
  • Functionals.jl; contains what I hope is a Julian approach

I need to do some complicated operations deep down in my source code and have them configurable by the user. I have coded up an OOP approach which stores complicated functions in a type and passing that as an argument; see Objects.jl. For a more Julian approach I create a concrete type and make it callable with different arguments from inside Main.jl. I’ve also written a more hard coded approach which is in Work.jl.

Using BenchmarkTools gives me some strange results. The OOP and hard-coded approaches run in the same amount of time while my Julian attempt takes 2-3 times longer. The Julian attempt does far more allocations too.

Any comments and improvements are welcome!

My version info:

julia> versioninfo()
Julia Version 0.6.4-pre.2
Commit 003c43eed2* (2018-06-05 13:07 UTC)
Platform Info:
  OS: macOS (x86_64-apple-darwin17.5.0)
  CPU: Intel(R) Core(TM) i7-5557U CPU @ 3.10GHz
  WORD_SIZE: 64
  BLAS: libopenblas (USE64BITINT DYNAMIC_ARCH NO_AFFINITY Haswell MAX_THREADS=16)
  LAPACK: libopenblas64_
  LIBM: libopenlibm
  LLVM: libLLVM-3.9.1 (ORCJIT, broadwell)

#2

The problem is that this:

is not type-stable since the incoming value has type Bool and you’re building its value type based on its runtime value instead of its type information so the dynamic dispatch is made at each integration call instead of once at compilation time. There are a few ways this is solved.

  1. Pass in Val{true} instead of true, and then specialize on ::Type{T}) where T and pass that T down.
  2. Wait for v0.7 where literals are optimized as constants within the function call.
  3. Put a let block around which does let T=Val{∂} and then do the integrand definition and call, so that way the type is fixed in the local scope.
  4. Put a function barrier which passes in Val{∂} so that way it’s inferred, and then behind the barrier construct the integrand and solve.
  5. Don’t do https://gist.github.com/jwscook/e8440d7f76c92b4fb622b58b899f12c2#file-main-jl-L10-L15 . If you’re following DiffEq for this style, note that I think it’s cool but probably a mistake and DiffEq will be going away from this the moment kwargs are fast enough. If you just make a type which holds two functions and if/else which field to call, then you cut out dispatch entirely here by not relying on type information at all. And if you do want to rely on type information, you could always do 1-4 on the type + 2 functions setup, so it’s not necessarily worse anyways. I would use value-type dispatch as an API only if you have an undefined number of functions you want passed in but somehow can generate type-stable calls to them.

#3

Thanks for your in depth reply!

I implemented your suggestions. See below for appraisals + snippets.

  1. This improved performance to within a factor of ~1.5 of the others.
function work(fun::T, d::V) where {T<:Functionals.Abstract, V}
  integrand(x::U) where {U<:Number} = kernel(x) * T(x, d)
  return QuadGK.quadgk(integrand, fun.lower, fun.upper)[1]
end
  1. Won’t do this one yet.

  2. Same improvement as 1.

function work(fun::T, ∂::Bool) where {T<:Functionals.Abstract}
  let
    Δ = Val{∂}
    integrand(x::U) where {U<:Number} = kernel(x) * T(x, Δ)
    return QuadGK.quadgk(integrand, fun.lower, fun.upper)[1]
  end
end
  1. Same improvement as 1.
function work(fun::T, ∂::Bool) where {T<:Functionals.Abstract}
  function generateintegrand(v::V) where {V}
    integrand(x::U) where {U<:Number} = kernel(x) * T(x, v)
    return integrand
  end
  integrand = generateintegrand(Val{∂})
  return QuadGK.quadgk(integrand, fun.lower, fun.upper)[1]
end

5a. Equal performance with other implementations.

function work(fun::T, ::Type{Val{false}}) where {T<:Functionals.Abstract}
  integrand(x::U) where {U<:Number} = kernel(x) * T(x, Val{false})
  return QuadGK.quadgk(integrand, fun.lower, fun.upper)[1]
end
function work(fun::T, ::Type{Val{true}}) where {T<:Functionals.Abstract}
  integrand(x::U) where {U<:Number} = kernel(x) * T(x, Val{true})
  return QuadGK.quadgk(integrand, fun.lower, fun.upper)[1]
end

5b. Equal performance with other implementations.

function work(fun::T, ∂::Bool) where {T<:Functionals.Abstract}
  integrand(x::U) where {U<:Number} = ∂ ? kernel(x) * T(x, Val{true}) : kernel(x) * T(x, Val{false}) 
  return QuadGK.quadgk(integrand, fun.lower, fun.upper)[1]
end

My favourite solution is 5b, which will then be easily change to 2.

In my opinion the OOP approach is easier than the more Julian way just because ::Type{Val{true}} is complicated. If this is no longer required in 0.7 then I expect that the Julian method will be the easiest method for me.

edit: clarified language


#4

I wouldn’t call ::Type{Val{true}} the Julian way at all. Just make a type and stick two functions in it. If that’s what you’re calling the OOP approach, then note that it’s not a Julia anti-pattern and is used in things like Optim’s *Differentiable types as a nice way to pass a bundle of types down. Multiple dispatch is a good tool but there are ways to misuse it. Most uses of value types are probably abuse or incorrect use.


#5

Ahh. I was under the impression that it was an anti-pattern and I was missing out on some performance. Good to know!


#6

I wonder if all of the “Value Types” section in types should be moved to Performance Tips.


#7

Probably, there should be something about each kind of type in the docs about kinds of types.
Probably, half of that section merged with the docs on Value Types for Performance is of help.
AFAIK, the essential need for Val{ofthis} had been of performanticizing stuff & such.
AIK, the rapid advancement that is Julia has made old neediness new unneededness.
If there is reorg of those docs, emphasizing what may be important tooling forward is best.


#8

I think the anti-pattern is trying to use functions in a struct as a way of relying on an OOP crutch, instead of really looking at things, and seeing if there is a cleaner, more “Julian” way, using the type system, multiple dispatch, possibly even traits or metaprogramming. However, sometimes using a function “pointer” or a closure is exactly what you do need, and Julia does a good job these days handling that IMO. (I was so happy when the old performance penalties for anonymous functions went away!)


#9

What is your favourite solution to this particular problem? My preference is still a struct with a couple of functions, because it requires the fewest LOC for maximal performance (0.6.4), and allows you to give helpful names to the struct’s functions rather than calling T(x, d) (see e.g. enumeration 1).