Functions passed functions as arguments will automatically specialize if they call said function.
That is
inner(f,x) = f(x) # sort function args first to support `do` syntax
should specialize because it is calling f
, but
pass(f,x) = inner(f,x)
won’t necessarilly, because it isn’t calling f
.
Let’s use @noinline
to try and make this more clear.
function outer000(f) # 0 v 1 bools on specialization for outer, pass, inner
@noinline function inner(f,x)
return f(2*x)
end
@noinline function pass(f,x)
return inner(f,x)
end
out = zeros(1000)
for i = 1:1000
out[i] = pass(f,i)
end
out
end
function outer001(f) # 0 v 1 bools on specialization for outer, pass, inner
@noinline function inner(f::Fi,x) where {Fi}
return f(2*x)
end
@noinline function pass(f,x)
return inner(f,x)
end
out = zeros(1000)
for i = 1:1000
out[i] = pass(f,i)
end
out
end
function outer010(f) # 0 v 1 bools on specialization for outer, pass, inner
@noinline function inner(f,x)
return f(2*x)
end
@noinline function pass(f::Fp,x) where {Fp}
return inner(f,x)
end
out = zeros(1000)
for i = 1:1000
out[i] = pass(f,i)
end
out
end
function outer011(f) # 0 v 1 bools on specialization for outer, pass, inner
@noinline function inner(f::Fi,x) where {Fi}
return f(2*x)
end
@noinline function pass(f::Fp,x) where {Fp}
return inner(f,x)
end
out = zeros(1000)
for i = 1:1000
out[i] = pass(f,i)
end
out
end
function outer100(f::Fo) where {Fo} # 0 v 1 bools on specialization for outer, pass, inner
@noinline function inner(f,x)
return f(2*x)
end
@noinline function pass(f,x)
return inner(f,x)
end
out = zeros(1000)
for i = 1:1000
out[i] = pass(f,i)
end
out
end
function outer101(f::Fo) where {Fo} # 0 v 1 bools on specialization for outer, pass, inner
@noinline function inner(f::Fi,x) where {Fi}
return f(2*x)
end
@noinline function pass(f,x)
return inner(f,x)
end
out = zeros(1000)
for i = 1:1000
out[i] = pass(f,i)
end
out
end
function outer110(f::Fo) where {Fo} # 0 v 1 bools on specialization for outer, pass, inner
@noinline function inner(f,x)
return f(2*x)
end
@noinline function pass(f::Fp,x) where {Fp}
return inner(f,x)
end
out = zeros(1000)
for i = 1:1000
out[i] = pass(f,i)
end
out
end
function outer111(f::Fo) where {Fo} # 0 v 1 bools on specialization for outer, pass, inner
@noinline function inner(f::Fi,x) where {Fi}
return f(2*x)
end
@noinline function pass(f::Fp,x) where {Fp}
return inner(f,x)
end
out = zeros(1000)
for i = 1:1000
out[i] = pass(f,i)
end
out
end
This yields:
f(x) = exp(x)
julia> @btime outer000(f);
46.415 μs (1979 allocations: 38.84 KiB)
julia> @btime outer001(f);
47.216 μs (1979 allocations: 38.84 KiB)
julia> @btime outer010(f);
46.122 μs (1979 allocations: 38.84 KiB)
julia> @btime outer011(f);
45.612 μs (1979 allocations: 38.84 KiB)
julia> @btime outer100(f);
37.679 μs (1979 allocations: 38.84 KiB)
julia> @btime outer101(f);
38.109 μs (1979 allocations: 38.84 KiB)
julia> @btime outer110(f);
12.724 μs (1 allocation: 7.94 KiB)
julia> @btime outer111(f);
12.402 μs (1 allocation: 7.94 KiB)
It’s the two functions not calling f
that matter – they must either inline or specialize.