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.

Reply via email to