Some more discussion. The broadcast (.
) in Julia is a very powerful syntax. For example, given this simple function that takes three arguments:
julia> f(x,y,z) = x + y + sin(z)
f (generic function with 1 method)
julia> f(1.0,2.0,3.0)
3.1411200080598674
You can apply it to three vectors just by using the .
:
julia> f.([1.0,2.0],[3.0,4.0],[5.0,6.0])
2-element Vector{Float64}:
3.0410757253368614
5.720584501801074
Which roughly equivalent to the hand written loop:
julia> function g(x,y,z)
w = similar(x)
for i in eachindex(x)
w[i] = x[i] + y[i] + sin(z[i])
end
return w
end
g (generic function with 1 method)
julia> g([1.0,2.0],[3.0,4.0],[5.0,6.0])
2-element Vector{Float64}:
3.0410757253368614
5.720584501801074
Note
The broadcasting is actually more general because it promotes types for you - given vectors of integers it recognizes that the output is a float - and it only works if all the arrays have the same dimensions, which guarantees that all memory access are inbounds
, so that the broadcast may be faster than the loop that does not have that guarantee.
So you can “broadcast” any function you write yourself, and you get the “vectorized” version of the operation.
A “vectorized” function in other languages, R for example, is something like the loop above, except that the loop is written not in R, but in a lower-level language. In Julia the loop is fine, and the broadcasted or vectorized operations are mostly written in Julia under the hood.
Finally, note that a function like the one of this example does not even need to be written explicitly, because you can just use broadcasting from the beginning:
julia> x = [1.0,2.0]; y = [3.0,4.0]; z = [5.0,6.0];
julia> x .+ y .+ sin.(z)
2-element Vector{Float64}:
3.0410757253368614
5.720584501801074
julia> @. x + y + sin(z)
2-element Vector{Float64}:
3.0410757253368614
5.720584501801074
Just add the .
to every operation and you get the same result of the “vectorized” f
, and that will be lowered to a fused loop as f
is. The @.
macro helps one to broadcast everything (for long expressions missing a .
is easy in the first case).
The complication of your case is that you do not want to broadcast every operation of your function (you don’t want to check tau
for every element). Yet, a small test does not show any performance difference between the two cases:
julia> function f1(z, tau)
@assert tau > 0 ArgumentError("tau < 0")
z + tau
end
f1 (generic function with 1 method)
julia> function f2(z, tau)
z + tau
end
f2 (generic function with 1 method)
julia> z = rand(1000);
julia> @btime f1.($z, 1.0);
437.112 ns (1 allocation: 7.94 KiB)
julia> @btime f2.($z, 1.0);
438.706 ns (1 allocation: 7.94 KiB)
It is possible that the compiler is smart enough and only checks tau
once.