# Performance of loops

**URL:** <https://discourse.julialang.org/t/performance-of-loops/118273>\
**Category:** New to Julia\
**Tags:** broadcast, loops, aliasing\
**Created:** [August 16, 2024, 1:04pm UTC](https://discourse.julialang.org/t/performance-of-loops/118273 "2024-08-16T13:04:17Z")\
**Posts on this page:** 20\
**Page:** 1

<div class="post-metadata">

**Author:** ![Matthijs\_1971](https://avatars.discourse-cdn.com/v4/letter/m/bb73d2/32.png) [@Matthijs\_1971](https://discourse.julialang.org/u/Matthijs_1971)\
**Post date:** [August 16, 2024, 1:04pm UTC](https://discourse.julialang.org/t/performance-of-loops/118273/1 "2024-08-16T13:04:17Z")

</div>

Julia has several ways to define loops or repeat code, each with advantages and disadvantages. I am looking at the performance of some also inspired by previous posts on this forum.

```julia
#!/usr/bin/env julia

# https://discourse.julialang.org/t/map-vs-loops-array-comprehensions-in-julia-1-0/16137
# Note, this thread is from 2018.

import BenchmarkTools
import Random

function loop_with_map(x)
    x = map(a -> 2 * a, x)
    return x
end

function loop_with_map_dot(x)
    x .= map(a -> 2 * a, x) # Slowest
    return x
end

function loop_list_comprehension(x)
    # A list comprehension creates a new local scope and is therefore slow.
    x = [2 * val for val in x]
    return x
end

function loop_list_comprehension_dot(x)
    # A list comprehension creates a new local scope and is therefore slow.
    x .= [2 * val for val in x]
    return x
end

function for_loop(x)
    for i in range(1, length(x))
        x[i] = 2 * x[i]
    end
    return x
end

function for_loop_opt(x)
    for i in eachindex(x)
        @inbounds x[i] = 2 * x[i]
    end
    return x
end

function loop_via_broadcast(x)
    @. x = 2 * x
    return x
end

function setup_function(n)
    return Random.rand(Int64, n)
end

for n in [100, 1000, 10_000, 100_000, 1_000_000]
    println("\n\nn = ", n)
    BenchmarkTools.@btime loop_with_map(x_vec) setup = (x_vec = setup_function($n)) # Slow
    BenchmarkTools.@btime loop_with_map_dot(x_vec) setup = (x_vec = setup_function($n)) # Very slow
    BenchmarkTools.@btime loop_list_comprehension(x_vec) setup = (x_vec = setup_function($n)) # Slow
    BenchmarkTools.@btime loop_list_comprehension_dot(x_vec) setup = (x_vec = setup_function($n)) # Very slow
    BenchmarkTools.@btime for_loop(x_vec) setup = (x_vec = setup_function($n)) # Fast
    BenchmarkTools.@btime for_loop_opt(x_vec) setup = (x_vec = setup_function($n)) # Fast
    BenchmarkTools.@btime loop_via_broadcast(x_vec) setup = (x_vec = setup_function($n)) # Fast
end

```

Output:

```julia
2024_08_16 14:18:30 ~/temp $ loop_performance.jl

n = 100
  89.105 ns (1 allocation: 896 bytes)
  163.902 ns (1 allocation: 896 bytes)
  138.964 ns (1 allocation: 896 bytes)
  163.821 ns (1 allocation: 896 bytes)
  12.209 ns (0 allocations: 0 bytes)
  11.278 ns (0 allocations: 0 bytes)
  12.198 ns (0 allocations: 0 bytes)

n = 1000
  1.049 μs (1 allocation: 7.94 KiB)
  1.180 μs (1 allocation: 7.94 KiB)
  1.119 μs (1 allocation: 7.94 KiB)
  1.140 μs (1 allocation: 7.94 KiB)
  80.231 ns (0 allocations: 0 bytes)
  80.838 ns (0 allocations: 0 bytes)
  81.080 ns (0 allocations: 0 bytes)

n = 10000
  7.885 μs (2 allocations: 78.17 KiB)
  11.209 μs (2 allocations: 78.17 KiB)
  8.743 μs (2 allocations: 78.17 KiB)
  11.228 μs (2 allocations: 78.17 KiB)
  1.239 μs (0 allocations: 0 bytes)
  1.215 μs (0 allocations: 0 bytes)
  1.306 μs (0 allocations: 0 bytes)

n = 100000
  116.843 μs (2 allocations: 781.30 KiB)
  171.743 μs (2 allocations: 781.30 KiB)
  119.491 μs (2 allocations: 781.30 KiB)
  174.575 μs (2 allocations: 781.30 KiB)
  15.298 μs (0 allocations: 0 bytes)
  14.419 μs (0 allocations: 0 bytes)
  15.221 μs (0 allocations: 0 bytes)

n = 1000000
  1.428 ms (2 allocations: 7.63 MiB)
  2.420 ms (2 allocations: 7.63 MiB)
  1.420 ms (2 allocations: 7.63 MiB)
  2.573 ms (2 allocations: 7.63 MiB)
  696.670 μs (0 allocations: 0 bytes)
  664.293 μs (0 allocations: 0 bytes)
  698.923 μs (0 allocations: 0 bytes)
2024_08_16 14:21:31 ~/temp $  

```

Questions:

1. `loop_with_map`: `map` has been reported to be slow before, but a long time ago. I am surprised that it is slow today. Am I doing something wrong?
2. `loop_with_map_dot`: This uses broadcasting to place the result of the calculation back in `x`. The idea is that no new memory for `x` has to be allocated; memory allocation is slow. Obviously this makes `loop_with_map_dot` slower, not faster. I think the reason is that `map` already allocates memory and that the broadcast makes that the old memory of `x` has to be filled instead of just making `x` point to the memory created by `map`. More on broadcasting below. Correct?
3. `loop_list_comprehension`: List comprehensions create their own scope which makes them slow. But I expected this creation of a new scope to take fixed time, i.e. independent of the number of times the loop is executed. What is going on?
4. `loop_list_comprehension_dot`: Just the same as in `loop_with_map_dot`, the broadcast serves to re-use the memory that `x` points to, but as a result all of the values have to be pumped over. Correct?
5. `for_loop_opt`: Skips index checks. Why is this faster than `loop_via_broadcast`? It suggests (falsely?) that `loop_via_broadcast` _does_ a range check while this can’t fail since there is only one container involved. If two with different lengths were involved, I can imagine that a range check makes sense.
6. `loop_via_broadcast`: here the re-use of the memory of `x` actually works. This has been clarified [here](https://discourse.julialang.org/t/broadcasting-slower-than-for-loop/107562/5). Now I wonder if this is the _only_ time this works, in essence.

---

<div class="post-metadata">

**Author:** ![Elrod](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/elrod/32/22461_2.png) [@Elrod](https://discourse.julialang.org/u/Elrod)\
**Post date:** [August 16, 2024, 1:32pm UTC](https://discourse.julialang.org/t/performance-of-loops/118273/2 "2024-08-16T13:32:02Z")

</div>

> [@Matthijs\_1971](#):
>
> Obviously this makes `loop_with_map_dot` slower, not faster. I think the reason is that `map` already allocates memory and that the broadcast makes that the old memory of `x` has to be filled instead of just making `x` point to the memory created by `map`. More on broadcasting below. Correct?

Yes.

You can use `map!` if you want a map that updates the input, instead of allocating a result vector. Definitely prefer `map!(f, x)` over `x .= map(f, x)`.

---

<div class="post-metadata">

**Author:** ![mkitti](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/mkitti/32/12459_2.png) [@mkitti](https://discourse.julialang.org/u/mkitti)\
**Post date:** [August 16, 2024, 1:34pm UTC](https://discourse.julialang.org/t/performance-of-loops/118273/3 "2024-08-16T13:34:32Z")

</div>

> [@Elrod](#):
>
> map!(f, x)

A two argument `map!` does not exist in `Base`.

---

<div class="post-metadata">

**Author:** ![Elrod](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/elrod/32/22461_2.png) [@Elrod](https://discourse.julialang.org/u/Elrod)\
**Post date:** [August 16, 2024, 2:10pm UTC](https://discourse.julialang.org/t/performance-of-loops/118273/4 "2024-08-16T14:10:55Z")

</div>

`map!(f, x, x)`, then.  
E.g.

```julia
map!(x -> x + x, x, x)

```

---

<div class="post-metadata">

**Author:** ![mkitti](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/mkitti/32/12459_2.png) [@mkitti](https://discourse.julialang.org/u/mkitti)\
**Post date:** [August 16, 2024, 2:13pm UTC](https://discourse.julialang.org/t/performance-of-loops/118273/5 "2024-08-16T14:13:43Z")

</div>

The [docs warn against the arguments sharing memory.](https://docs.julialang.org/en/v1/base/collections/#Base.map!)

```julia
│ Warning
  │
  │ Behavior can be unexpected when any mutated argument
  │ shares memory with any other argument.

```

---

<div class="post-metadata">

**Author:** ![mikmoore](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/mikmoore/32/31109_2.png) [@mikmoore](https://discourse.julialang.org/u/mikmoore)\
**Post date:** [August 16, 2024, 2:35pm UTC](https://discourse.julialang.org/t/performance-of-loops/118273/6 "2024-08-16T14:35:09Z")

</div>

[Link to #50824](https://github.com/JuliaLang/julia/pull/50824) that added that aliasing warning and has discussion and links to some related issues. While `map!(f, x, x)` is not currently _guaranteed_ to work, it is relatively uncontroversial that it _had ought to_ work in most circumstances. If you’re feeling slightly brave, you’ll probably never run into issues when the input is identical to the output (but more complicated aliasing could still be problematic). But broadcasting does guarantee that simple aliasing like `x .= 2 .* x` will work, so probably safer to use that (see also `broadcast!`).

---

<div class="post-metadata">

**Author:** ![mbauman](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/mbauman/32/31082_2.png) [@mbauman](https://discourse.julialang.org/u/mbauman)\
**Post date:** [August 16, 2024, 2:41pm UTC](https://discourse.julialang.org/t/performance-of-loops/118273/9 "2024-08-16T14:41:29Z")

</div>

> [@mikmoore](#):
>
> While `map!(f, x, x)` is not currently _guaranteed_ to work, it is relatively uncontroversial that it _had ought to_ work in most circumstances. If you’re feeling slightly brave, you’ll probably never run into issues when the input is identical to the output (

Simple counter-example with severe consequences:

```julia
julia> v = zeros(2); map!(x->x+1, v, v); v
2-element Vector{Float64}:
 1.0
 1.0

julia> using SparseArrays

julia> v = spzeros(2); map!(x->x+1, v, v); v
2-element SparseVector{Float64, Int64} with 2 stored entries:
  [1] = 2.0
  [2] = 2.0

```

---

<div class="post-metadata">

**Author:** ![Matthijs\_1971](https://avatars.discourse-cdn.com/v4/letter/m/bb73d2/32.png) [@Matthijs\_1971](https://discourse.julialang.org/u/Matthijs_1971)\
**Post date:** [August 16, 2024, 2:48pm UTC](https://discourse.julialang.org/t/performance-of-loops/118273/10 "2024-08-16T14:48:24Z")

</div>

Professionally, I never feel brave, sorry. My company makes extremely expensive machines and downtime of these machines is also very expensive… and rework of the products of these machines is also costly. So I am looking for the safest, securest ways for loops without loosing performance so I can promote such ways to colleagues.

---

<div class="post-metadata">

**Author:** ![mikmoore](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/mikmoore/32/31109_2.png) [@mikmoore](https://discourse.julialang.org/u/mikmoore)\
**Post date:** [August 16, 2024, 2:52pm UTC](https://discourse.julialang.org/t/performance-of-loops/118273/11 "2024-08-16T14:52:52Z")

</div>

> [@mbauman](#):
>
> `julia> v = spzeros(2); map!(x->x+1, v, v); v`

`SparseArrays` has a specialized method for `map!`, which seems to cause the issue (but could potentially be improved, at least for the trivial case of `===` output with an input). But in any case,

```julia-repl
julia> v = spzeros(2); broadcast!(x->x+1, v, v); v
2-element SparseVector{Float64, Int64} with 2 stored entries:
  [1] = 1.0
  [2] = 1.0

```

so `broadcast!` is a suitable replacement for `map!`.

---

<div class="post-metadata">

**Author:** ![DNF](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/dnf/32/10191_2.png) [@DNF](https://discourse.julialang.org/u/DNF)\
**Post date:** [August 16, 2024, 3:00pm UTC](https://discourse.julialang.org/t/performance-of-loops/118273/12 "2024-08-16T15:00:41Z")

</div>

> [@Matthijs\_1971](#):
>
> ```julia
> function for_loop_opt(x)
> for i in eachindex(x)
> @inbounds x[i] = 2 * x[i]
> end
> return x
> end
> 
> function loop_via_broadcast(x)
> @. x = 2 * x
> return x
> end
> 
> ```

In that case, use one of the two above, with some added remarks:

- Add a trailing `!` to the function names to indicate mutation.
- Remove `@inbounds`, it already happens automatically with `eachindex`.
- you can use the nice update operator `*=`, so that `a = 2 * a` becomes `a *= 2`.

---

<div class="post-metadata">

**Author:** ![DNF](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/dnf/32/10191_2.png) [@DNF](https://discourse.julialang.org/u/DNF)\
**Post date:** [August 16, 2024, 3:03pm UTC](https://discourse.julialang.org/t/performance-of-loops/118273/13 "2024-08-16T15:03:59Z")

</div>

> [@Matthijs\_1971](#):
>
> List comprehensions create their own scope which makes them slow.

I don’t know about this. Isn’t it just the allocation that’s the problem?

---

<div class="post-metadata">

**Author:** ![mbauman](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/mbauman/32/31082_2.png) [@mbauman](https://discourse.julialang.org/u/mbauman)\
**Post date:** [August 16, 2024, 3:32pm UTC](https://discourse.julialang.org/t/performance-of-loops/118273/14 "2024-08-16T15:32:20Z")

</div>

Yes, broadcasting is much more conservative when it comes to aliasing — `broadcast!` _doesn’t_ have that warning, but you’ll also see that it (sometimes) allocates more than you’d expect. It errs on the side of (sometimes) making surprising defensive copies, which you may not want in cases where you’re chasing peak performance — which points us back on topic a bit more here.

In this case, with arrays that use the default broadcast implementation, the `loop_via_broadcast` sees that it iterates both the LHS and RHS “in step” with eachother and thus new outputs won’t overwrite yet-to-be-read inputs… and so the implementation needn’t make that defensive copy.

The most important takeaways when looking at all of these different ways of performing `2x` is that:

- Assignment (the single `=`) is just about **names** that you’re choosing to use to describe things. Choosing to change what a name means doesn’t have anything to do with “memory re-use”.
- All arguments are **always evaluated** — separately! — before being passed to a function. It can sometimes be tricky to see what makes for a separate function call — especially in the face of broadcast dots and other syntax sugars, though.

Finally, and perhaps most mind-bendingly,

- The superpower of Julia is that the code you write can be as fast — or faster — than its “builtins.” Broadcasting _is complicated_, far more complicated than your simple loop. And simple is often fast.

---

<div class="post-metadata">

**Author:** ![Matthijs\_1971](https://avatars.discourse-cdn.com/v4/letter/m/bb73d2/32.png) [@Matthijs\_1971](https://discourse.julialang.org/u/Matthijs_1971)\
**Post date:** [August 16, 2024, 4:18pm UTC](https://discourse.julialang.org/t/performance-of-loops/118273/15 "2024-08-16T16:18:30Z")

</div>

[The weak evidence about scope in list comprehensions is here.](https://docs.julialang.org/en/v1/manual/variables-and-scoping/#man-scope-table-1). Checking this is above my pay grade. Also, I think a simple function has its own scope, so I doubt now my own statement. I got it from Discourse somewhere though, but I can’t find it back.

---

<div class="post-metadata">

**Author:** ![stephancb](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/stephancb/32/14243_2.png) [@stephancb](https://discourse.julialang.org/u/stephancb)\
**Post date:** [August 16, 2024, 4:26pm UTC](https://discourse.julialang.org/t/performance-of-loops/118273/16 "2024-08-16T16:26:20Z")

</div>

> [@DNF](#):
>
> Add a trailing `!` to the function names to indicate mutation.

`loop_with_map` and `loop_list_comprehension` actually do not mutate (to also my surprise), the others do so.

---

<div class="post-metadata">

**Author:** ![Matthijs\_1971](https://avatars.discourse-cdn.com/v4/letter/m/bb73d2/32.png) [@Matthijs\_1971](https://discourse.julialang.org/u/Matthijs_1971)\
**Post date:** [August 16, 2024, 4:38pm UTC](https://discourse.julialang.org/t/performance-of-loops/118273/17 "2024-08-16T16:38:35Z")

</div>

> [@stephancb](#):
>
> `loop_with_map` and `loop_list_comprehension` actually do not mutate (to also my surprise), the others do so.

That is baffling indeed. Great catch. Although off-topic, if anybody understands this, please share!

---

<div class="post-metadata">

**Author:** ![mbauman](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/mbauman/32/31082_2.png) [@mbauman](https://discourse.julialang.org/u/mbauman)\
**Post date:** [August 16, 2024, 4:45pm UTC](https://discourse.julialang.org/t/performance-of-loops/118273/18 "2024-08-16T16:45:20Z")

</div>

`loop_with_map` and `loop_list_comprehension` are both of the form:

```julia
function f(x)
    x = #... something
end

```

This is just deciding you have a better use _for the name `x`_ within the context of that function body. What `x` happened to be (or not) before the re-assignment has no effect. It’s just a name for us humans, no different than initially using the name “Matt” for me and then deciding that you’d rather use it for yourself. You’re not changing anything about either me or you by changing our nicknames!

---

<div class="post-metadata">

**Author:** ![Matthijs\_1971](https://avatars.discourse-cdn.com/v4/letter/m/bb73d2/32.png) [@Matthijs\_1971](https://discourse.julialang.org/u/Matthijs_1971)\
**Post date:** [August 16, 2024, 5:51pm UTC](https://discourse.julialang.org/t/performance-of-loops/118273/19 "2024-08-16T17:51:16Z")

</div>

This is just deciding you have a better use _for the name `x`_ within the context of that function body. What `x` happened to be (or not) before the re-assignment has no effect. It’s just a name for us humans, no different than initially using the name “Matt” for me and then deciding that you’d rather use it for yourself. You’re not changing anything about either me or you by changing our nicknames!  
[/quote]

Correct, I spoke in haste. If `x` is assigned a new object in the function, the old object is unchanged and found back after the function is terminated. If the object is essentially re-used, it is mutated and the changed values are found back on the caller side. I understand this.

---

<div class="post-metadata">

**Author:** ![sgaure](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/sgaure/32/14779_2.png) [@sgaure](https://discourse.julialang.org/u/sgaure)\
**Post date:** [August 16, 2024, 7:00pm UTC](https://discourse.julialang.org/t/performance-of-loops/118273/20 "2024-08-16T19:00:11Z")

</div>

> [@Matthijs\_1971](#):
>
> [The weak evidence about scope in list comprehensions is here.](https://docs.julialang.org/en/v1/manual/variables-and-scoping/#man-scope-table-1). Checking this is above my pay grade. Also, I think a simple function has its own scope, so I doubt now my own statement. I got it from Discourse somewhere though, but I can’t find it back.

List comprehensions create a closure, i.e. an anonymous function, so definitely a new scope. However, a new scope does not slow anything down (except possibly the compiler, though I doubt it’s very significant).

---

<div class="post-metadata">

**Author:** ![abraemer](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/abraemer/32/51403_2.png) [@abraemer](https://discourse.julialang.org/u/abraemer)\
**Post date:** [August 16, 2024, 8:01pm UTC](https://discourse.julialang.org/t/performance-of-loops/118273/21 "2024-08-16T20:01:28Z")

</div>

> [@Matthijs\_1971](#):
>
> So I am looking for the safest, securest ways for loops without loosing performance so I can promote such ways to colleagues.

To actually answer your question: Just write the loop. It is simple, expressive and fast. Everything else (like broadcasting) is eye candy/convenience in the end. You can still (and actually) should use the appropriate iteration function.

So the best version of your function (which incorporates the tips by @DNF):

```julia
function for_loop_opt!(x)
    for i in eachindex(x)
        x[i] *= 2
    end
    return x
end

```

This

- is easy to read and understand even when you are not a Julia professional
- does not allocate
- has no possible out-of-bounds memory access (`@inbounds` is dangerous!)
- actually infers the `inbounds` property if the compiler can prove it (e.g. for standard `Array`s)

---

<div class="post-metadata">

**Author:** ![DNF](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/dnf/32/10191_2.png) [@DNF](https://discourse.julialang.org/u/DNF)\
**Post date:** [August 16, 2024, 8:20pm UTC](https://discourse.julialang.org/t/performance-of-loops/118273/22 "2024-08-16T20:20:54Z")

</div>

> [@stephancb](#):
>
> `loop_with_map` and `loop_list_comprehension` actually do not mutate

But I was specifically referring to `for_loop_opt` and `loop_via_broadcast`, which both do.

[Next page](https://discourse.julialang.org/t/performance-of-loops/118273.md?page=2)
