Usage
LazyBroadcast.jl
provides a way to defer the execution of a broadcasted expression. As we show in the Quick start, this can signficantly improve performance of Julia code.
The basic usage of this package involves wrapping intermediate broadcast expressions with dot-calls to lazy_broadcast
, and then working with those resulting objects. Let's make one now.
using LazyBroadcast;
x = rand(10);
y_lazy = lazy_broadcast.(x .+ x);
Base.Broadcast.Broadcasted{Base.Broadcast.DefaultArrayStyle{1}}(+, ([0.44752658836561143, 0.646212755434696, 0.6220028662167782, 0.02123521714084442, 0.9050266654380316, 0.10161130304368826, 0.6246855465093148, 0.1558109063918074, 0.23949417157006703, 0.13827831223621356], [0.44752658836561143, 0.646212755434696, 0.6220028662167782, 0.02123521714084442, 0.9050266654380316, 0.10161130304368826, 0.6246855465093148, 0.1558109063918074, 0.23949417157006703, 0.13827831223621356]))
As you can see, y_lazy
is a Broadcasted
object. There are several things that you can do with them:
materialize
them (which executes the expression)- Combine with other broadcast expressions
- Call functions that support
Broadcasted
objects
Let's try each of these.
If you've not read the Julia Base Broadcast Background section, that may be helpful in understanding concepts, types, and functions discussed here.
Materialize
Materializing consists in evaluating a Broadcasted
object: when you materialize an object, you execute the expression and get the result back. To materialize an expression, we call Base.Broadcast.materialize
y_array = Base.Broadcast.materialize(y_lazy)
10-element Vector{Float64}:
0.8950531767312229
1.292425510869392
1.2440057324335565
0.04247043428168884
1.8100533308760631
0.20322260608737652
1.2493710930186297
0.3116218127836148
0.47898834314013405
0.2765566244724271
Or, we can call the in-place version, Base.Broadcast.materialize!
:
z_array = similar(x)
Base.Broadcast.materialize!(z_array, y_lazy)
z_array
10-element Vector{Float64}:
0.8950531767312229
1.292425510869392
1.2440057324335565
0.04247043428168884
1.8100533308760631
0.20322260608737652
1.2493710930186297
0.3116218127836148
0.47898834314013405
0.2765566244724271
Let's take a second to reflect on what we did here: starting from an array (x
), we created an unevaluated expression that manipulates that array (y_lazy
, representing x .+ x
), and we deferred the evaluation of that array to a later time.
We can take this to a step further: after we create y_lazy
, we can manipulate it and combine it with other Broadcasted
expression, deferring the evaluation of the full expression to a later time. This opens up a new world of possibilities and optimizations. Let us explore them in the next section.
Combine with other broadcast expressions
Once we have Broadcasted
expressions, we can combine them with more dot operations:
z_lazy = lazy_broadcast.(x .* x);
fused_lazy = y_lazy .+ z_lazy # Equivalent to `x .+ x .+ x .* x`
10-element Vector{Float64}:
1.0953332240253864
1.7100164361558943
1.6308932980154438
0.04292136872870765
2.6291265960299457
0.21354746299361277
1.639603125036271
0.33589885133425135
0.5363458013561667
0.2956775161073229
In this combined expression, y_lazy .+ z_lazy
is equivalent to x .+ x .+ x .* x
, which means that this entire expression is fused. If we want to further delay execution, we can wrap the entire result in a dot-lazy_broadcast
call:
so_lazy = lazy_broadcast.(y_lazy .+ z_lazy)
Base.Broadcast.Broadcasted{Base.Broadcast.DefaultArrayStyle{1}}(+, (Base.Broadcast.Broadcasted{Base.Broadcast.DefaultArrayStyle{1}}(+, ([0.44752658836561143, 0.646212755434696, 0.6220028662167782, 0.02123521714084442, 0.9050266654380316, 0.10161130304368826, 0.6246855465093148, 0.1558109063918074, 0.23949417157006703, 0.13827831223621356], [0.44752658836561143, 0.646212755434696, 0.6220028662167782, 0.02123521714084442, 0.9050266654380316, 0.10161130304368826, 0.6246855465093148, 0.1558109063918074, 0.23949417157006703, 0.13827831223621356])), Base.Broadcast.Broadcasted{Base.Broadcast.DefaultArrayStyle{1}}(*, ([0.44752658836561143, 0.646212755434696, 0.6220028662167782, 0.02123521714084442, 0.9050266654380316, 0.10161130304368826, 0.6246855465093148, 0.1558109063918074, 0.23949417157006703, 0.13827831223621356], [0.44752658836561143, 0.646212755434696, 0.6220028662167782, 0.02123521714084442, 0.9050266654380316, 0.10161130304368826, 0.6246855465093148, 0.1558109063918074, 0.23949417157006703, 0.13827831223621356]))))
Now, we have a Broadcasted
object again.
Note that manipulating Broadcasted
objects is very cheap, so LazyBroadcast
allows you to construct a complex expression that can be evaluated more efficiently later.
Call functions that support Broadcasted
Several functions in Julia Base directly support operations on Broadcasted
objects. For example, sum
:
sum(so_lazy)
10.129363679783003
As we can see, this is equivalent to first evaluating the Broadcasted
expression and then calling sum
on the result.
Another common function that directly supports Broadcasted
object is Base.copyto!
, which can be called naturally through broadcast expressions (@.
-> materialize!
-> copyto!
, see Julia Base Broadcast Background for details):
my_array = similar(x)
@. my_array = so_lazy
10-element Vector{Float64}:
1.0953332240253864
1.7100164361558943
1.6308932980154438
0.04292136872870765
2.6291265960299457
0.21354746299361277
1.639603125036271
0.33589885133425135
0.5363458013561667
0.2956775161073229