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?