Internals of how lazy_broadcast
works
We recommend reading Julia Base Broadcast Background before this section.
The entire implementation of LazyBroadcast is very short, and can be written in a few lines of code:
using Base.Broadcast: broadcasted, materialize, instantiate
function lazy_broadcast end
struct LazyBroadcasted{T}
value::T
end
Base.Broadcast.broadcasted(::typeof(lazy_broadcast), x) = LazyBroadcasted(x)
Base.materialize(x::LazyBroadcasted) = instantiate(x.value)
Let's break down how this works with a simple example:
x = [1, 2]
y = [1, 2]
z = x .+ y
First, the code is lowered, and we can see what the code is lowered to using Julia's meta-programming utilities:
julia> Meta.@lower(x .+ y)
:($(Expr(:thunk, CodeInfo(
@ none within `top-level scope`
1 ─ %1 = Base.broadcasted(+, x, y)
│ %2 = Base.materialize(%1)
└── return %2
))))
Now, you can see that our expression is transformed into a CodeInfo
object, which contains a sequence of steps. If we back-substitute these expressions, we get Base.materialize(Base.broadcasted(+, x, y))
, which is exactly what LazyBroadcast.code_lowered_single_expression(:(x .+ y))
returns:
julia> LazyBroadcast.code_lowered_single_expression(:(x .+ y))
:(Base.materialize(Base.broadcasted(+, x, y)))
Note that Base.materialize
calls Base.Broadcast.instantiate
on the input argument, so let's take a look at the instantiated argument to Base.materialize
:
julia> Base.Broadcast.instantiate(Base.broadcasted(+, x, y))
Base.Broadcast.Broadcasted{Base.Broadcast.DefaultArrayStyle{1}}(+, ([1, 2], [1, 2]))
Here's where the magic comes in. Looking back at the implementation from above, we know that
- When we broadcast
lazy_broadcast
over an expressionx
,Base.broadcasted
returnsLazyBroadcasted(expr)
, and Base.materialize(::LazyBroadcast)
simply returnsx
So, the result is:
julia> bc = LazyBroadcast.lazy_broadcast.(x .+ y)
Base.Broadcast.Broadcasted{Base.Broadcast.DefaultArrayStyle{1}}(+, ([1, 2], [1, 2]))
In other words, we get back the instantiated argument to Base.materialize
. What's important to note here is that no computations occur until Base.materialize
, so LazyBroadcast.lazy_broadcast.(x .+ y)
returns a "lazy" object (a Base.Broadcasted
object) that we can launch computations on, at any moment, by calling Base.materialize(bc)
.