When should I use broadcasting in Julia instead of a loop?

I’m still getting used to Julia’s broadcasting syntax, especially the use of . with functions and operators.

For simple cases, something like:

y = sin.(x) .+ 1

is very convenient.

But once the expression gets a little more complicated, I sometimes find an explicit loop easier to read.

So I’m wondering: in normal Julia code, when do you personally prefer broadcasting, and when would you switch to a loop?

Is this mostly a readability choice, or are there performance differences I should keep in mind as well?

I’m mainly trying to build good habits early rather than automatically adding dots everywhere.

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.

Welcome to the community! Note that you can use @. in this case:

y = @. sin(x) + 1

I love wrapping stuff into functions and then calling them with the dot. It’s very satisfying to do this :slight_smile:

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).

Can you give an example of these two cases?

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.

In which sense?

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

Right, broadcasting performs array expansion unlike for loops, list comprehensions and map.