# Semantically equivalent expressions with wildly different performance

**URL:** <https://discourse.julialang.org/t/semantically-equivalent-expressions-with-wildly-different-performance/131123>\
**Category:** General Usage\
**Tags:** macros, broadcasting, wat\
**Created:** [July 28, 2025, 4:40am UTC](https://discourse.julialang.org/t/semantically-equivalent-expressions-with-wildly-different-performance/131123 "2025-07-28T04:40:52Z")\
**Posts on this page:** 11\
**Page:** 1

<div class="post-metadata">

**Author:** ![bremez](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/bremez/32/38777_2.png) [@bremez](https://discourse.julialang.org/u/bremez)\
**Post date:** [July 28, 2025, 4:40am UTC](https://discourse.julialang.org/t/semantically-equivalent-expressions-with-wildly-different-performance/131123/1 "2025-07-28T04:40:52Z")

</div>

I came across the following example when I needed something that beats the performance of `LinearAlgebra`’s build in `mul!(M, x, y')`. Any known reason for this huge difference in performance? Tested on 11.6.

```julia-auto
using BenchmarkTools
function outer1!(output, x, y)
    for j in eachindex(y)
        @. (view(output, :, j)) = x * y[j]
    end
    return output
end

function outer2!(output, x, y)
    for j in eachindex(y)
        v = view(output, :, j)
        @. (v) = x * y[j]
    end
    return output
end

x = rand(1000); y = rand(1000); M = rand(1000,1000);
outer1!(M, x, y) ≈ outer2!(M, x, y) 
#true

@btime outer1!(M, x, y);
# ~10 ns
@btime outer2!(M, x, y);
# ~200 μs

```

---

<div class="post-metadata">

**Author:** ![technocrat](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/technocrat/32/220947_2.png) [@technocrat](https://discourse.julialang.org/u/technocrat)\
**Post date:** [July 28, 2025, 4:58am UTC](https://discourse.julialang.org/t/semantically-equivalent-expressions-with-wildly-different-performance/131123/2 "2025-07-28T04:58:41Z")

</div>

If I understand this correctly (and I might be wrong about this as about so much else):

**`outer1!` (fast)**: When `@.` sees the inline `view(output, :, j)`, it can perform much more aggressive optimization, possibly:

- Eliminating the view allocation entirely
- Directly generating optimized assembly for the memory access pattern
- Better vectorization since the compiler sees the full expression structure

**`outer2!` (slow)**: Pre-computing the view actually prevents these optimizations:

- The view object must be created and stored
- The `@.` macro works on a pre-existing view, limiting optimization opportunities
- The compiler can’t see through the abstraction as effectively

---

<div class="post-metadata">

**Author:** ![bremez](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/bremez/32/38777_2.png) [@bremez](https://discourse.julialang.org/u/bremez)\
**Post date:** [July 28, 2025, 5:14am UTC](https://discourse.julialang.org/t/semantically-equivalent-expressions-with-wildly-different-performance/131123/3 "2025-07-28T05:14:05Z")

</div>

Nah, I found the issue. The first implementation doesn’t do what I hope it does. The bogus true comparison is because I used the same output buffer… instead

```julia-auto
x = rand(1000); y = rand(1000); M1 = zeros(1000,1000); M2 = copy(M1);
outer1!(M1, x, y) ≈ outer2!(M2, x, y) # false

```

Whoopsy!

---

<div class="post-metadata">

**Author:** ![Benny](https://avatars.discourse-cdn.com/v4/letter/b/49beb7/32.png) [@Benny](https://discourse.julialang.org/u/Benny)\
**Post date:** [July 28, 2025, 5:44am UTC](https://discourse.julialang.org/t/semantically-equivalent-expressions-with-wildly-different-performance/131123/4 "2025-07-28T05:44:09Z")

</div>

Yeah it basically ran faster because it didn’t do anything:

```julia-auto
julia> outer1!(copy(M), x, y) ≈ M
true

julia> M ≈ outer2!(copy(M), x, y)
false

```

Looks like `@.` failure causing a _method_ expression. This example of overlapping syntax causing a silent error probably counts as a [WAT](https://discourse.julialang.org/t/a-most-harrowing-collection-of-julia-wats/77323), so I suggest adding a #wat tag for future discoverability.

```julia-auto
julia> @macroexpand @. (view(output, :, j)) = x * y[j]
:(view(output, :, j) = begin
          #= REPL[51]:1 =#
          (*).(x, y[j])
      end)

julia> let
       view(output, :, j) = begin
               #= REPL[51]:1 =#
               (*).(x, y[j])
           end
       end
(::var"#view#3") (generic function with 1 method)

julia> @macroexpand @. (v) = x * y[j]
:(v .= (*).(x, y[j]))

julia> @macroexpand (view(output, :, j)) .= x .* y[j] # manual fix
:(view(output, :, j) .= x .* y[j])

```

A fix after which:

```julia-auto
julia> outer1!(copy(M), x, y) ≈ outer2!(copy(M), x, y)
true

julia> @btime outer1!($(copy(M)), $x, $y);
  248.700 μs (0 allocations: 0 bytes)

julia> @btime outer2!($(copy(M)), $x, $y);
  249.000 μs (0 allocations: 0 bytes)

```

Benchmarks that mutate their inputs are generally bad but this might be fine for runtime because there are no conditionals.

---

<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:** [July 28, 2025, 6:49am UTC](https://discourse.julialang.org/t/semantically-equivalent-expressions-with-wildly-different-performance/131123/5 "2025-07-28T06:49:27Z")

</div>

It’s not clear to me what the dotted `view`

```julia-auto
view.(output, :, j)

```

ought to mean. Does it create a view for each element in `output`? But in that case, why doesn’t it fail with a bounds error for `j > 1`?

---

<div class="post-metadata">

**Author:** ![Benny](https://avatars.discourse-cdn.com/v4/letter/b/49beb7/32.png) [@Benny](https://discourse.julialang.org/u/Benny)\
**Post date:** [July 28, 2025, 7:33am UTC](https://discourse.julialang.org/t/semantically-equivalent-expressions-with-wildly-different-performance/131123/6 "2025-07-28T07:33:54Z")

</div>

There isn’t a dotted view, `@.` ignores left hand expressions to support method definitions.

---

<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:** [July 28, 2025, 8:14am UTC](https://discourse.julialang.org/t/semantically-equivalent-expressions-with-wildly-different-performance/131123/7 "2025-07-28T08:14:14Z")

</div>

Huh? Since when does `@.` ignore left hand side? Sorry for the sidetrack, but I’ve _never_ heard anything like that. I thought it dotted _every_ call.

I find no mention of this in the docs

> **[Arrays · The Julia Language](https://docs.julialang.org/en/v1/base/arrays/#Base.Broadcast.%40 __dot__ )**
>
> Documentation for The Julia Language.

---

<div class="post-metadata">

**Author:** ![Benny](https://avatars.discourse-cdn.com/v4/letter/b/49beb7/32.png) [@Benny](https://discourse.julialang.org/u/Benny)\
**Post date:** [July 28, 2025, 8:53am UTC](https://discourse.julialang.org/t/semantically-equivalent-expressions-with-wildly-different-performance/131123/8 "2025-07-28T08:53:02Z")

</div>

It’s not documented, but it’s how it’s [implemented](https://github.com/JuliaLang/julia/blob/9615af0f269df4d371b8010e9507ed5bae86103b/base/broadcast.jl#L1263).

---

<div class="post-metadata">

**Author:** ![technocrat](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/technocrat/32/220947_2.png) [@technocrat](https://discourse.julialang.org/u/technocrat)\
**Post date:** [July 28, 2025, 10:10am UTC](https://discourse.julialang.org/t/semantically-equivalent-expressions-with-wildly-different-performance/131123/9 "2025-07-28T10:10:09Z")

</div>

I made a wildly uneducated guess.

---

<div class="post-metadata">

**Author:** ![bremez](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/bremez/32/38777_2.png) [@bremez](https://discourse.julialang.org/u/bremez)\
**Post date:** [July 28, 2025, 2:42pm UTC](https://discourse.julialang.org/t/semantically-equivalent-expressions-with-wildly-different-performance/131123/10 "2025-07-28T14:42:04Z")

</div>

Great spot and thanks for the WAT tag suggestion.

Side remark about the macro expansion: I was always under the vague impression that `@.` is the only way to fuse broadcasts, which otherwise remain separate. Yet it seems to expand (in your second-to-last example) to what I had thought is not fused. So is the snippet `v .= x .* y` fused, or does it allocate a temporary array in the multiplication?

---

<div class="post-metadata">

**Author:** ![Benny](https://avatars.discourse-cdn.com/v4/letter/b/49beb7/32.png) [@Benny](https://discourse.julialang.org/u/Benny)\
**Post date:** [July 28, 2025, 2:54pm UTC](https://discourse.julialang.org/t/semantically-equivalent-expressions-with-wildly-different-performance/131123/11 "2025-07-28T14:54:10Z")

</div>

Sequential dots fuse, and `@.` is actually rarer than manually writing dots. It is sometimes faster to not fuse everything into 1 loop despite the extra allocations, and `@.` can’t make that call:

```julia-auto
julia> longid(x) = (Libc.systemsleep(0.5); x) # expensive call
longid (generic function with 1 method)

julia> @time reshape(1:2, 1, 2) .* longid.(1:2) # fused, only allocates output
  2.044504 seconds (2 allocations: 112 bytes)
2×2 Matrix{Int64}:
 1 2
 2 4

julia> @time reshape(1:2, 1, 2) .* identity(longid.(1:2)) # not fused, also allocates intermediate
  1.012347 seconds (4 allocations: 192 bytes)
2×2 Matrix{Int64}:
 1 2
 2 4

```

An explicit 2-level loop would let you store the `longid` call in a temporary variable in the outer loop and save the time in the 1st example and the allocation in the 2nd example, but despite the limitation to calling the fused kernel in an apparent innermost loop, broadcasting does also automatically handle indexing order and expand singleton dimensions, which may be easier to maintain and apply to more inputs.

You can check what an expression does if it gets a bit complicated:

```julia-auto
julia> Meta.@lower v .= x .* y # fused before 1 materialize
:($(Expr(:thunk, CodeInfo(
    @ none within `top-level scope`
1 ─ %1 = v
│ %2 = Base.broadcasted(*, x, y)
│ %3 = Base.materialize!(%1, %2)
└── return %3
))))

julia> Meta.@lower v .= identity(x .* y) # 2 separate materialize
:($(Expr(:thunk, CodeInfo(
    @ none within `top-level scope`
1 ─ %1 = v
│ %2 = identity
│ %3 = Base.broadcasted(*, x, y)
│ %4 = Base.materialize(%3)
│ %5 = (%2)(%4)
│ %6 = Base.broadcasted(Base.identity, %5)
│ %7 = Base.materialize!(%1, %6)
└── return %7
))))

```
