I only use broadcasting when its single line if its 2 line I just make the loop to fuse them. I don’t know if its the right thing to do but for micro optim I always endup doing it.
In very broad strokes, expressions like broadcasting that abstract away implementation details are often simpler to write and have more potential to be compiled for more contexts, but the overlap in capability is actually a lot smaller than people expect. Loops don’t have to get very complicated before broadcasting is unsuitable, like an inherent sequence or a conditional break; some loops have different “shorthands” like Einstein notation (Tullio.jl is a relatively common but not the only implementation). On the other hand, it can get very annoying to reimplement the boilerplate of broadcasted array shapes in a manual loop, and it’s much harder to write such a loop to work on various shapes. In some cases like CUDA.jl, broadcasting the supported operations on either CPU or GPU arrays is seamless, but there are explicit differences between writing a sequential loop versus a CUDA kernel or a specifically annotated parallel loop e.g. the now stagnant Floops.jl. Ultimately, the programmer must decide how to write the code for their intent, which includes how the code could be reused in the future.
Broadcasting is useful when you have multiple arrays of different shapes (or different number of dimensions) that are still “compatible”. For applying a function to an array without changing any shapes, or to apply a function to multiple arrays with the same exact shape, there is the map function. It’s convenient both for small functions (write inline) and for more complex ones (use do block).
You should use broadcast always when you are processing matrices and vectors. Use loops or comprehensions for a more elaborated algorithms. Just a matter of use. You will not write a loop for process only a vector.
Basically, broadcasting to work with arrays of different (compatible) shapes:
julia> A = [1 2; 3 4; 5 6]
julia> b = [1, 2, 4]
julia> A .+ b
3×2 Matrix{Int64}:
2 3
5 6
9 10
While map is natural for single-array operations, and more convenient to write longer operations:
julia> map(b) do x
y = x + 1
z = sin(x^2)
(x, z)
end
3-element Vector{Tuple{Int64, Float64}}:
(1, 0.8414709848078965)
(2, -0.7568024953079282)
(4, -0.2879033166650653)
It works with short operations of course, but here broadcasting can read nicer:
julia> map(x -> x^2, b)
3-element Vector{Int64}:
1
4
16