# Why is broadcast of getproperty so slow?

**URL:** https://discourse.julialang.org/t/why-is-broadcast-of-getproperty-so-slow/129970
**Category:** Performance
**Created:** [June 17, 2025, 12:27pm UTC](https://discourse.julialang.org/t/why-is-broadcast-of-getproperty-so-slow/129970 "2025-06-17T12:27:27Z")
**Posts on this page:** 14
**Page:** 1

<div class="post-metadata">

### Author: ![mwlidar](https://avatars.discourse-cdn.com/v4/letter/m/6de8d8/32.png) [@mwlidar](https://discourse.julialang.org/u/mwlidar)
#### Post date: [June 17, 2025, 12:27pm UTC](https://discourse.julialang.org/t/why-is-broadcast-of-getproperty-so-slow/129970/1 "2025-06-17T12:27:28Z")

</div>

Looking on the following code I found that a simple broadcast of getproperty is about two to three times slower than a simple for loop implementation:

```julia
struct MyStruct
    x::Float64
    y::Float64
    z::Float64
end

#xyz = fill((x = 2.0, y = 3.0, z = 4.0), 50000)
xyz = fill(MyStruct(2.0, 3.0, 4.0), 50000)

function sl(xyz)
    c = Vector{Float64}(undef, length(xyz))
    for i in eachindex(c, xyz)
        c[i] = xyz[i].x
    end
    c
end

function sl2(xyz)
    getproperty.(xyz, :x)
end

display(@benchmark sl($xyz))
display(@benchmark sl2($xyz))

```

Output is:

```julia
BenchmarkTools.Trial: 10000 samples with 1 evaluation per sample.
 Range (min … max): 21.000 μs … 266.773 ms ┊ GC (min … max): 0.00% … 99.93%
 Time (median): 98.800 μs ┊ GC (median): 0.00%
 Time (mean ± σ): 168.594 μs ± 4.606 ms ┊ GC (mean ± σ): 47.31% ± 1.73%

    █ ▂ ▁
  ▂▃█▇▃▂▂▂▁▂▁▁▂▁▁▁▁▁▂▃▃▄▅▆████▇▆▅▅▄▄▃▃▃▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂▂ ▃
  21 μs Histogram: frequency by time 208 μs <

 Memory estimate: 390.69 KiB, allocs estimate: 3.
BenchmarkTools.Trial: 10000 samples with 1 evaluation per sample.
 Range (min … max): 84.300 μs … 293.450 ms ┊ GC (min … max): 0.00% … 99.90%
 Time (median): 211.100 μs ┊ GC (median): 0.00%
 Time (mean ± σ): 282.448 μs ± 4.864 ms ┊ GC (mean ± σ): 29.80% ± 1.73%

         ▂▅ ▁█▇▃▄▅▅▄▄▂
  ▂▂▃▂▅▅▂██▇▄▂▂▁▂▁▁▁▁▁▂▁▂▂▁▂▂▂▂▃▃▅███████████▇▆▄▄▃▃▃▃▃▃▃▂▂▂▂▂▂▂ ▄
  84.3 μs Histogram: frequency by time 298 μs <

 Memory estimate: 390.69 KiB, allocs estimate: 3.

```

With a vector of named tuples the broadcast is even slower, while the loop keeps about the same speed. The generated code of the broadcasting version is also extremely large for such a simple gathering copy.

This is with 1.11.5 on Windows and a x86\_64 CPU.

Is this reproducible on other architectures?

Seems like coding “FORTRAN style” is still the best way to full performance in Julia, which is a bit sad…

---

<div class="post-metadata">

### Author: ![lmiq](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/lmiq/32/18314_2.png) [@lmiq](https://discourse.julialang.org/u/lmiq)
#### Post date: [June 17, 2025, 12:50pm UTC](https://discourse.julialang.org/t/why-is-broadcast-of-getproperty-so-slow/129970/2 "2025-06-17T12:50:36Z")

</div>

I’m not sure what is the difference there, but this version seems to be even faster:

```julia
sl4(xyz) = [el.x for el in xyz]

```

```julia-repl
julia> using Chairmarks

julia> @b sl4($xyz)
12.004 μs (3 allocs: 390.695 KiB)

julia> @b sl($xyz)
16.970 μs (3 allocs: 390.695 KiB)

```

And it is the broadcasting that it slow, not the `getproperty` itself:

```julia
julia> sl5(xyz) = [getproperty(el, :x) for el in xyz]
sl5 (generic function with 1 method)

julia> @b sl5($xyz)
12.024 μs (3 allocs: 390.695 KiB)

```

---

<div class="post-metadata">

### Author: ![nilshg](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/nilshg/32/2283_2.png) [@nilshg](https://discourse.julialang.org/u/nilshg)
#### Post date: [June 17, 2025, 12:52pm UTC](https://discourse.julialang.org/t/why-is-broadcast-of-getproperty-so-slow/129970/3 "2025-06-17T12:52:55Z")

</div>

I can reproduce on 1.12-beta4, same OS and architecture.

If you don’t like the loop maybe a comprehension instead?

```julia
julia> function sl3(xyz)
           [i.x for i ∈ xyz]
       end

julia> @b sl2($test)
108.300 μs (3 allocs: 390.694 KiB)

julia> @b sl3($test)
25.800 μs (3 allocs: 390.694 KiB)

```

---

<div class="post-metadata">

### Author: ![mwlidar](https://avatars.discourse-cdn.com/v4/letter/m/6de8d8/32.png) [@mwlidar](https://discourse.julialang.org/u/mwlidar)
#### Post date: [June 17, 2025, 1:13pm UTC](https://discourse.julialang.org/t/why-is-broadcast-of-getproperty-so-slow/129970/4 "2025-06-17T13:13:56Z")

</div>

I don’t ‘like’ the loop, because its not a one-liner, but much more code. I didn’t try the comprehension, because in former times I had read some post here where comprehensions were the slowest possibility. But that seemingly has changed with newer Jula version…

---

<div class="post-metadata">

### Author: ![aplavin](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/aplavin/32/222056_2.png) [@aplavin](https://discourse.julialang.org/u/aplavin)
#### Post date: [June 17, 2025, 1:16pm UTC](https://discourse.julialang.org/t/why-is-broadcast-of-getproperty-so-slow/129970/5 "2025-06-17T13:16:34Z")

</div>

Broadcasting `f.(x)`, loops, comprehensions, and `map(f, x)` generally have the same performance – if they are type-stable.

In general, `getproperty(x, prop)` is not type-stable: if properties of `x` have different types, its return type depends on the _value_ of `prop`, not just the _type_ of `prop` (which is always `Symbol`). Sometimes, Julia is able to avoid this instability by constant-propagation (as in `getproperty(x, :name)`), but apparently constprop doesn’t propagate through broadcasting.

A performance advice is not to rely on constprop if you can avoid it. For example, here use either those comprehesions suggested above or `map(i -> i.x, xyz)`.  
Or, if accessing columns is common, use StructArrays.jl instead of regular arrays: there, extracting a column is not just cheap, but free.

---

<div class="post-metadata">

### Author: ![gdalle](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/gdalle/32/27854_2.png) [@gdalle](https://discourse.julialang.org/u/gdalle)
#### Post date: [June 17, 2025, 1:16pm UTC](https://discourse.julialang.org/t/why-is-broadcast-of-getproperty-so-slow/129970/6 "2025-06-17T13:16:56Z")

</div>

How about `map(Base.Fix2(getproperty, :x), xyz)`?

---

<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: [June 17, 2025, 1:25pm UTC](https://discourse.julialang.org/t/why-is-broadcast-of-getproperty-so-slow/129970/7 "2025-06-17T13:25:35Z")

</div>

As others have mentioned, the challenge here is generally that `getproperty(something, :field)` requires some serious constant propagation of `:field` in order to avoid dynamic-ness. You’re running the value `:x` through some complicated machinery where it’s easy to lose track of its constant-ness. The simple solution is to move the `:x` “as close as possible” to its actual use. For example:

```Julia
julia> getx(xyz) = xyz.x # or getproperty(xyz, :x), it doesn't matter anymore
getx (generic function with 1 method)

julia> function sl3(xyz)
           getx.(xyz)
       end
sl3 (generic function with 1 method)

julia> display(@benchmark sl3($xyz))
BenchmarkTools.Trial: 10000 samples with 1 evaluation per sample.
 Range (min … max): 14.083 μs … 5.195 ms ┊ GC (min … max): 0.00% … 98.65%
 Time (median): 28.416 μs ┊ GC (median): 0.00%
 Time (mean ± σ): 39.684 μs ± 139.454 μs ┊ GC (mean ± σ): 29.73% ± 11.94%

  ▅█▆▂ ▁
  ████▇▄▄▃▃▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▃▅▁▃▅▃▅▄▅▆▇ █
  14.1 μs Histogram: log(frequency) by time 476 μs <

 Memory estimate: 416.06 KiB, allocs estimate: 3.

```

(for reference, `sl` has a minimum time of 23.3 µs, and sl2 is 110 µs)

---

<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: [June 17, 2025, 2:47pm UTC](https://discourse.julialang.org/t/why-is-broadcast-of-getproperty-so-slow/129970/8 "2025-06-17T14:47:34Z")

</div>

The compile-time constant is not propagated through the broadcast, see [#43333](https://github.com/JuliaLang/julia/issues/43333). As discussed in this thread and in that issue, the workaround is to use a function (or comprehension, etc) that “hard-codes” the property instead. For example, `(a -> a.x).(xyz)`.

`Base.Fix2` does not help here. It creates a closure over the _value_ of the second argument (noting only the captured argument’s type ~~in the type domain~~ at compile time) and so has the same issue as the broadcasted property.

---

<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: [June 17, 2025, 9:59pm UTC](https://discourse.julialang.org/t/why-is-broadcast-of-getproperty-so-slow/129970/9 "2025-06-17T21:59:10Z")

</div>

I don’t think the missing feature is constant propagation through higher order functions to their input functions, it’s that dot syntax can’t really control what inputs are hardcoded into the fused broadcast kernel versus being iterated. Currently it’s just keyword arguments going into the kernel (which is its own [Issue #34737](https://github.com/JuliaLang/julia/issues/34737) because sometimes we would like to broadcast over those) and positional arguments being iterated (which we often opt out of by iterating over a `Ref` wrapper instead, but that’s not the same as putting it right in the kernel). There isn’t an obvious motive or way to put values in the kernel either; as pointed out with `Base.Fix2`, storing a value in a parametric field of a callable (which isn’t iterated) can actually impede constant propagation, and capturing variables of inputs (in other cases, not here with `:x`) is semantically different and can cause an often fundamentally unfixable type instability ([Issue #15276](https://github.com/JuliaLang/julia/issues/15276)).

---

<div class="post-metadata">

### Author: ![e3c6](https://avatars.discourse-cdn.com/v4/letter/e/e79b87/32.png) [@e3c6](https://discourse.julialang.org/u/e3c6)
#### Post date: [June 18, 2025, 7:21am UTC](https://discourse.julialang.org/t/why-is-broadcast-of-getproperty-so-slow/129970/10 "2025-06-18T07:21:34Z")

</div>

Couldn’t the compiler do things like these automatically?

---

<div class="post-metadata">

### Author: ![Mason](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/mason/32/2423_2.png) [@Mason](https://discourse.julialang.org/u/Mason)
#### Post date: [June 18, 2025, 9:43am UTC](https://discourse.julialang.org/t/why-is-broadcast-of-getproperty-so-slow/129970/11 "2025-06-18T09:43:00Z")

</div>

In theory, yes of course, a sufficiently smart compiler could absolutely do that. But if you read what Matt said:

> As others have mentioned, the challenge here is generally that `getproperty(something, :field)` requires some **serious constant propagation of `:field` in order to avoid dynamic-ness**. You’re running the value `:x` through some complicated machinery where it’s easy to lose track of its constant-ness.

he’s just saying that doing this sort of thing is generally hard for a compiler. At one point the compiler knows that `x` is const, but then it has to propagate that information through a **bunch** of layers of complicated functions and broadcast machinery, and at some point along the way, the compiler appears to have lost the information that `:x` was a constant value, so it’s no longer able to make the appropriate optimization.

Our compiler is generally getting smarter over time and getting better at doing constant propagation / concrete evaluation and all sorts of other tricks, but it’s still a real compiler that has real constraints and it doesn’t always perfectly figure everything out.

---

<div class="post-metadata">

### Author: ![liuyxpp](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/liuyxpp/32/9870_2.png) [@liuyxpp](https://discourse.julialang.org/u/liuyxpp)
#### Post date: [June 18, 2025, 3:14pm UTC](https://discourse.julialang.org/t/why-is-broadcast-of-getproperty-so-slow/129970/12 "2025-06-18T15:14:58Z")

</div>

I wonder whether we can have a lint feature to at least notify people the code (and other cases) could be slow due to such type instability?

---

<div class="post-metadata">

### Author: ![Mason](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/mason/32/2423_2.png) [@Mason](https://discourse.julialang.org/u/Mason)
#### Post date: [June 18, 2025, 3:32pm UTC](https://discourse.julialang.org/t/why-is-broadcast-of-getproperty-so-slow/129970/13 "2025-06-18T15:32:42Z")

</div>

This case isn’t actually type unstable, since `getproperty(::MyStruct, ::Symbol)::Float64`. It’s just more branch-ey.

---

<div class="post-metadata">

### Author: ![aryavorskiy](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/aryavorskiy/32/43466_2.png) [@aryavorskiy](https://discourse.julialang.org/u/aryavorskiy)
#### Post date: [July 13, 2025, 5:53pm UTC](https://discourse.julialang.org/t/why-is-broadcast-of-getproperty-so-slow/129970/14 "2025-07-13T17:53:58Z")

</div>

Broadcasting treats any object as a scalar (i. e. not broadcasting over its axes) if it is wrapped into a `Ref`. This is how `Symbol`s were made scalars in the first place. Since references are dynamic by nature, we can say goodbye to constant propagation and also spend time looking up the reference on each iteration.

A hacky fix would be to store the symbol as a type parameter, so const propagation can take it into account:

```julia
struct BroadcastConstContainer{T} end
Base.Broadcast.BroadcastStyle(::Type{<:BroadcastConstContainer}) = Base.Broadcast.DefaultArrayStyle{0}()
Base.size(::BroadcastConstContainer) = (1,)
Base.getindex(::BroadcastConstContainer{T}, I) where T = T
Base.Broadcast.broadcastable(sym::Symbol) = BroadcastConstContainer{sym}()

```

After that performance of `sl2` even surpasses `sl` and gets closer to that of `sl4` and similar.  
Also note that `nothing`, `missing` and some more simple types are also using `Ref` with broadcast under the hood, which can also lead to performance regressions. Maybe this should be implemented into base Julia in some way?..

[UPD. Read Benny’s message above. This is almost the same, but the object is not callable — does it fix the problem?]
