On Thu, Jul 2, 2015 at 9:33 AM, Dan Schmidt <[email protected]> wrote:
> I was writing some sample code to demonstrate to coworkers how dispatching
> on types was faster than calling functions passed in as arguments, and ran
> into a performance issue I can't explain.
>
> This is running on JuliaBox, but I observed the same behavior on a local
> machine, both with 0.3.9 and with a 2 days old master of 0.4.0. All timings
> were made after ensuring that the code was jitted.
>
> Julia Version 0.4.0-dev+5491
> Commit cb77503 (2015-06-21 09:45 UTC)
>
>
> # Pass in a function
> function map_f( v::Vector{Float64}, f::Function )
> ret = copy( v )
> for i in 1:length(ret)
> ret[i] = f( ret[i] )
> end
> return ret
> end;
>
> v = collect(1.0:1000000.0);
> mul_two( x::Float64 ) = x * 2.0;
>
> @time map_f( v, mul_two );
>
> 115.911 milliseconds (4000 k allocations: 70319 KB, 5.76% gc time)
>
>
> # Pass in an object associated with the function
> function map_f( v::Vector{Float64}, f_type )
> ret = copy( v )
> for i in 1:length(ret)
> ret[i] = call_f( f_type, ret[i] )
> end
> return ret
> end;
> type MulTwo end
> call_f( ::MulTwo, x::Float64 ) = x * 2.0;
>
> @time map_f( v, MulTwo() );
> 4.283 milliseconds (6 allocations: 7813 KB)
>
>
> So far so good. But what if I pass call_f the type itself instead of an
> object of that type?
>
> call_f( ::Type{MulTwo}, x::Float64 ) = x * 2.0;
>
> @time map_f( v, MulTwo );
>
> 45.678 milliseconds (2000 k allocations: 39063 KB, 14.27% gc time)
>
>
> Lots more time, lots and lots more allocations. I looked at the generated
> code of map_f with @code_warntype and both versions were exactly the same
> except for the type ascribed to f_type in the Variables: section. I won't
> clutter this message with the output unless it's requested.
>
> So I tried running julia with --track-allocation=user and finally found a
> difference:
>
> - function map_f( v::Vector{Float64}, f_type )
> 8887450 ret = copy( v )
> 0 for i in 1:length(ret)
> 0 ret[i] = call_f( f_type, ret[i] )
> - end
> 0 return ret
> - end;
> -
> - type MulTwo end
> -
> - call_f( ::MulTwo, x::Float64 ) = x * 2.0;
>
> vs
>
> - function map_f( v::Vector{Float64}, f_type )
> 8887226 ret = copy( v )
> 0 for i in 1:length(ret)
> 16000000 ret[i] = call_f( f_type, ret[i] )
> - end
> 0 return ret
> - end;
> -
> - type MulTwo end
> -
> 16000000 call_f( ::Type{MulTwo}, x::Float64 ) = x * 2.0;
> -
>
> So despite the generated code appearing to be exactly the same, somehow lots
> of allocations are going on in the version that dispatches on a type value
> rather than on a typed object.
>
> What exactly is going on here, and is there a way that I could have figured
> it out on my own?
>
>
Not sure if you could easily figure this one out but the issue here is
that julia only specialize on the `Type{}` of a datatype if you
specify so.
i.e. `f(x) = ...` called with `f(Int)` will only specialize on
`f(::DataType)`. If you want to specialize on `f(::Type{Int})`, you
need to either define `f(::Type{Int})` or more generalized
`f{T}(::Type{T})`
In your example. your second `map_f` is only specialized on
`map_f(::Vector{Float64}, ::DataType)` when called with the type (in a
sense not so much better than the first version). Therefore, the call
to `call_f` need to go through runtime dispatch which involve a whole
bunch of random stuff including boxing (which allocates). You can fix
this by defining `map_f{T}(v::Vector{Float64}, f_type::Type{T})`
instead.
If you are on 0.4, the `call_f` can be replaced with a `call` overload
and the second version should be the perferred way to do this and in
the long term this is very likely how anonymous functions (or all
functions) works.