You may be familiar with writing functions like this to be generic:
function mysume(v::AbstractVector)
s = zero(eltype(V))
@simd for x in v
s += x
end
s
end
This function should be type stable for all reasonable definitions of +
. Eg:
xf = rand(100);
@code_warntype mysume(xf)
xi = rand(1:100, 100);
@code_warntype mysume(xi)
using StaticArrays
xm = [@SMatrix randn(2,2) for _ in 1:100];
@code_warntype mysume(xm)
You can also write the function like this:
function mysumw(v::AbstractVector{T}) where {T}
s = zero(T)
@simd for x in v
s += x
end
s
end
and you’ll get the same result: it uses T
, the element type parameter, to make a zero of the appropriate type.
Julia compiles a specialized version for each element type, one for T === Float64
, another for T === Int
, and another for T === SMatrix{2,2,Float64,4}
.
What if instead of using a type parameter T
to give us an element type and let us make a zero of the correct type, we used an actual value as a type parameter?
AbstractArray
s sort of all do this, giving us their number of dimensions. Check this out:
julia> twice_ndim(x) = 2length(size(x))
twice_ndim (generic function with 1 method)
julia> @code_typed twice_ndim(rand(3))
CodeInfo(
1 ─ Base.arraysize(x, 1)::Int64
└── return 2
) => Int64
julia> @code_typed twice_ndim(rand(3,3))
CodeInfo(
1 ─ Base.arraysize(x, 1)::Int64
│ Base.arraysize(x, 2)::Int64
└── return 4
) => Int64
julia> @code_typed twice_ndim(rand(3,3,3))
CodeInfo(
1 ─ Base.arraysize(x, 1)::Int64
│ Base.arraysize(x, 2)::Int64
│ Base.arraysize(x, 3)::Int64
└── return 6
) => Int64
The code for twice_ndim
doesn’t calculate anything (no 2*
): it just returns the answer!
Tamas_Papp was taking advantage of this, by using a Val type. You can make your own via:
struct MyVal{T} end
the trick is instead of using T
to describe the struct, like how an Array{Float64,2}
's type parameters say it (a) holds Float64
elements and (b) has 2
dimensions, MyVal
’s T
can be anything.
But, just like the Array{T,N}
's N
let the twice_ndim
function compile into simply spitting out the answer:
julia> @code_typed fibval(Val(46))
CodeInfo(
1 ─ goto #3 if not false
2 ─ nothing::Nothing
3 ┄ return 2971215073
) => Int64
julia> @btime fibval(Val(46))
0.001 ns (0 allocations: 0 bytes)
2971215073
fibval
here isn’t doing anything except returning the answer.
The first run that compiled was also very fast. fib
is slow, because normally fib(N)
requires O(2^N)
work. However, with Tamas_Papp’s trick – which is equivalent to memoization – requires just O(N)
work, because each fib(::Val{N})
only gets calculated once, and the result saved into a specialized method.
Compiling takes vastly longer than simple arithmetic. Dynamic dispatches, if the types of everything couldn’t be found at compile time, also take much longer (although a dynamic dispatch takes much less time than compilation).
It will normally be better to let the compiler create a function optimized to do arithmetic on values quickly, than compile a version of the function specific to those values. The exceptions are things like StaticArrays
, where some of the values – eg, the size of the arrays – can be used to help the compiler compute on the values of the array more quickly. Other types would include things like special matrix properties, eg, is a matrix positive definite, or maybe lower triangular?
When it comes to values like data (eg, contents of the arrays), it would almost certainly be better to NOT use these values as type parameters. Just write a normal function and assign the results to a variable, or maybe use a memoization library.