This is a question about efficiency. Suppose I have a routine which performs an operation in the same manner on an array irrespective of whether the entries are Int, Float or Complex. The output type will be an array with the same data-type. How can I implement that without overloading the function with different argument types ?
For example, consider the simple function
function f(a::Array{Float64,1}) ::Array{Float64,1}
for i in 1:size(a,1)
a[i]*=i;
end
end
The actual body of the funciton is not relevant, in reality it will be something more complicated. If I want this function to work for Int64 and Complex{Float64} arrays too, do I need to repeat the definition but with different argument types ? I have read in Julia’s performance enhancing tips that specifying the data type can lead to more efficient use of time and memory. IS there a trade-off ?
As a general rule, it is important to label fields of a struct or the elements of a container you are constructing with their types, but you do not need to label the types of your function inputs for performance. In fact, it says so right here in the manual: Performance Tips · The Julia Language
In many languages with optional type declarations, adding declarations is the principal way to make code run faster. This is not the case in Julia. In Julia, the compiler generally knows the types of all function arguments, local variables, and expressions.
What that means is that any of the following function definitions will give exactly the same performance:
function f1(a) # matches any `a`; equivalent to `a::Any`
...
end
function f2(a::AbstractArray) # matches an array with any number of dimensions
...
end
function f3(a::AbstractVector) # matches an array with exactly one dimension
...
end
function f4(a::AbstractVector{<: Number}) # matches an array with exactly one dimension
# whose elements are all some kind of `Number`
...
end
function f5(a::Vector{Float64}) # matches only exactly a Vector of Float64
...
end
Try it! Define each of those functions and install BenchmarkTools.jl via:
]add BenchmarkTools
and then do:
using BenchmarkTools
@btime f1($a)
@btime f2($a)
@btime f3($a)
@btime f4($a)
@btime f5($a)
So why would you choose f4
instead of f1
? Adding types to your function arguments is a matter of API design. If you define an f4(a::AbstractVector{<:Number})
, then you can also define f4(a::SomeOtherType)
and the compiler will automatically pick the appropriate method for whatever input is passed in. Or you might choose to specify your argument types as a signal to users of your code so that they know what you’re expecting them to provide.
11 Likes
This is extremely useful, including the comparative tests ! Thank you very much for your time and effort !
A follow-up question : suppose I wish to pass an argument which is Array of arbitrary type but exactly 2 dimensions. What would be the syntax ?
If you mean an array with 2 elements, like [3, 4]
, then the answer is that you cannot restrict the length of an array like that, since the type of a standard Julia array does not contain the information about its length.
A good alternative would be to use a tuple instead:
julia> f(x, (y, z)) = x * (y + z)
f (generic function with 1 method)
julia> f(1, (2, 3))
5
1 Like
Or you can check out StaticArrays for small fixed-size arrays: https://github.com/JuliaArrays/StaticArrays.jl
2 Likes
What you are probably looking for here is a Tuple
not an Array
. The difference is that Tuples
allow inference on small statically sized heterogeneous collections.
@Oscar_Smith @rdeits @dpsanders I apologize, I meant 2 dimensional array as input with unspecified data-type.
Ah, that would be f(a::AbstractMatrix)
or equivalently f(a::AbstractArray{T, 2}) where T
6 Likes