Mekanika Broadcast

Menyusun gaya broadcast kustom dan memahami perilaku alokasi memori pada broadcasting.

Daftar isi

Di balik sintaks titik terdapat mesin broadcast yang menyusun operasi sebagai pohon malas, memilih implementasinya melalui gaya broadcast, lalu mengeksekusinya dalam satu perulangan. Topik ini membahas objek Broadcasted, gaya broadcast bawaan dan kustom, serta biaya alokasi memori akibat fusing. Setelah topik ini dipahami, tipe array sendiri dapat diikutsertakan dalam broadcast dan keputusan alokasinya dapat dikendalikan secara sadar.

Dari sintaks titik menjadi Broadcasted#

Ekspresi bertitik tidak langsung dihitung. Julia menyusunnya sebagai objek Broadcasted yang menyimpan fungsi beserta seluruh argumennya, termasuk Broadcasted lain yang bersarang, lalu menghitung keseluruhan pohon itu ketika materialize dipanggil. Tipe hasil dan implementasinya ditentukan oleh gaya broadcast, yaitu objek yang diturunkan dari BroadcastStyle.

julia Contoh: gaya bawaan dan objek Broadcasted
using Base.Broadcast: BroadcastStyle, broadcasted, materialize

println(BroadcastStyle(Vector{Int}))
println(BroadcastStyle(Int))
println(BroadcastStyle(Matrix{Float64}))

bc = broadcasted(+, [1, 2], 10)
println(typeof(bc))
println(materialize(bc))
text Keluaran
Base.Broadcast.DefaultArrayStyle{1}()
Base.Broadcast.DefaultArrayStyle{0}()
Base.Broadcast.DefaultArrayStyle{2}()
Base.Broadcast.Broadcasted{Base.Broadcast.DefaultArrayStyle{1}, Nothing, typeof(+), Tuple{Vector{Int64}, Int64}}
[11, 12]

Array biasa memakai DefaultArrayStyle yang menyimpan banyaknya dimensi sebagai parameter tipe, sedangkan bilangan memakai DefaultArrayStyle{0} karena diperlakukan sebagai skalar. Keluaran materialize pada contoh adalah array biasa, karena gaya seluruh argumennya memang gaya bawaan.

Gaya broadcast dan penggabungannya#

Setiap tipe memiliki gaya yang disukai, dan mesin broadcast menggabungkan gaya seluruh argumen menjadi satu gaya penentu. Aturan penggabungannya ditulis sebagai metode biner BroadcastStyle, dan satu urutan argumen saja sudah cukup karena mesin broadcast menguji kedua urutan melalui result_style. Array biasa kalah dari gaya kustom yang berupa AbstractArrayStyle, sehingga tipe array sendiri tidak perlu menulis aturan apa pun untuk mengungguli DefaultArrayStyle.

julia Contoh: gaya kustom mengalahkan gaya bawaan
using Base.Broadcast: BroadcastStyle, ArrayStyle, DefaultArrayStyle, result_style

println(BroadcastStyle(ArrayStyle{Vector{Float64}}(), DefaultArrayStyle{1}()))
println(result_style(DefaultArrayStyle{2}(), ArrayStyle{Vector{Float64}}()))
text Keluaran
ArrayStyle{Vector{Float64}}()
ArrayStyle{Vector{Float64}}()

Pembungkus Broadcast.ArrayStyle{TipeSaya}() dipakai untuk tipe yang merupakan AbstractArray, sedangkan Broadcast.Style{TipeSaya}() tersedia untuk tipe arbitrer. Gaya yang menyimpan banyaknya dimensi, yaitu turunan AbstractArrayStyle{N}, dapat mengatur perpindahan dimensi melalui konstruktor Val(N)-nya, pola yang dipakai paket SparseArrays untuk beralih dari vektor sparse ke matriks sparse. Aturan biner hanya perlu ditulis apabila dua gaya kustom berbeda harus dipertemukan.

Gaya yang menang bukan sekadar label. Gaya penentu itulah yang dipakai untuk memilih metode similar pengalokasi keluaran dan metode copyto! pelaksana perhitungan, sehingga menang berarti memegang kendali penuh atas tipe keluaran beserta cara perhitungannya. Pada contoh di atas, keluaran bertipe Vector karena ArrayStyle{Vector{Float64}} tidak mendefinisikan keluaran khusus, tetapi metode pelaksananya tetap milik gaya yang menang.

Gaya broadcast kustom#

Contoh berikut menambahkan tipe Pengukuran, yaitu vektor yang membawa label, dan membuat hasil broadcast tetap membawa label yang sama. Tiga bagian yang dibutuhkan adalah deklarasi gaya, alokasi keluaran melalui similar, dan pelaksanaan melalui copyto!. Fungsi cari menelusuri pohon broadcast untuk menemukan argumen Pengukuran, termasuk yang tersembunyi di dalam cabang yang bersarang.

julia gaya-kustom.jl
# gaya-kustom.jl
# Struct Pengukuran membawa label pengukuran, dan label itu
# dipertahankan pada hasil broadcast melalui gaya broadcast kustom.

import Base: copyto!, similar, size, getindex, setindex!
import Base.Broadcast: Broadcasted, ArrayStyle, preprocess

struct Pengukuran <: AbstractVector{Float64}
    data::Vector{Float64}
    label::String
end

size(x::Pengukuran) = size(x.data)
getindex(x::Pengukuran, i::Int) = x.data[i]
setindex!(x::Pengukuran, v, i::Int) = setindex!(x.data, v, i)

# deklarasi gaya: setiap broadcast yang melibatkan Pengukuran
# memakai ArrayStyle{Pengukuran}
Base.Broadcast.BroadcastStyle(::Type{Pengukuran}) = ArrayStyle{Pengukuran}()

# pencarian Pengukuran di dalam pohon broadcast (dapat bersarang)
cari(bc::Broadcasted) = cari(bc.args)
cari(args::Tuple) = cari(cari(args[1]), Base.tail(args))
cari(x) = x
cari(::Tuple{}) = nothing
cari(p::Pengukuran, rest) = p
cari(::Any, rest) = cari(rest)

# alokasi keluaran: tetap Pengukuran, label diwarisi dari argumen
function similar(bc::Broadcasted{ArrayStyle{Pengukuran}}, ::Type{T}) where {T}
    p = cari(bc)
    label = p === nothing ? "campuran" : p.label
    Pengukuran(similar(Vector{T}, axes(bc)), label)
end

# pelaksanaan: satu perulangan per elemen
function copyto!(dest::Pengukuran, bc::Broadcasted{ArrayStyle{Pengukuran}})
    bc′ = preprocess(dest, bc)
    @inbounds for i in eachindex(dest)
        dest[i] = bc′[i]
    end
    return dest
end

p = Pengukuran([20.0, 25.0, 30.0], "suhu-ruang")

q = p .+ 273.15
println(q)
println(typeof(q), " dengan label ", q.label)

r = p .* 2 .+ 1
println(r)
println(typeof(r), " dengan label ", r.label)
text Keluaran
[293.15, 298.15, 303.15]
Pengukuran dengan label suhu-ruang
[41.0, 51.0, 61.0]
Pengukuran dengan label suhu-ruang

Keluaran q dan r tetap bertipe Pengukuran dengan label yang diwarisi, padahal operasi .* dan .+ bercampur dalam satu ekspresi terfusi. Pola tiga bagian itu persis pola yang dipakai paket-paket array pada ekosistem Julia, sehingga pola ini dapat diterapkan pada tipe array yang lebih serius. Perhatikan ketiga bagiannya. Deklarasi gaya menentukan bahwa setiap broadcast yang melibatkan Pengukuran menang atas array biasa. Metode similar mengalokasikan penampung keluaran, dan karena metode ini yang menentukan tipe keluaran, label diwariskan pada tahap alokasi. Metode copyto! mengeksekusi perhitungannya dalam satu perulangan, dengan preprocess menyiapkan argumen skalar agar dapat diindeks bersama array lainnya.

Fusing dan biaya alokasi memori#

Semua operasi titik dalam satu ekspresi dilebur menjadi satu perulangan, perilaku yang disebut fusing. Ekspresi a .* b .+ c disusun sebagai Broadcasted(+, Broadcasted(*, a, b), c) dan dieksekusi sekali jalan, sehingga keluaran baru dialokasikan tepat satu kali. Perhitungan yang dipisah ke variabel sementara tidak terfusi dan mengalokasikan array perantara tambahan. Skema berikut menggambarkan meleburnya pohon tersebut.

a .* b .+ c+a .* bcpohon Broadcasteddiratakany[i] = a[i]*b[i] + c[i]satu perulangan, satu alokasi
Pohon Broadcasted dari a .* b .+ c yang diratakan menjadi satu perulangan
julia fusing.jl
# fusing.jl
# Membandingkan alokasi memori antara ekspresi terfusi dan
# perhitungan bertahap. Pengukuran dilakukan setelah pemanasan.

using Random

rng = MersenneTwister(42)
a = rand(rng, 10_000)
b = rand(rng, 10_000)
c = rand(rng, 10_000)

f_terfusi(a, b, c) = a .* b .+ c

function f_bertahap(a, b, c)
    sementara = a .* b
    sementara .+ c
end

# pemanasan: kedua fungsi telah dikompilasi sebelum pengukuran
f_terfusi(a, b, c)
f_bertahap(a, b, c)

print("terfusi : ")
println(@allocated f_terfusi(a, b, c))
print("bertahap: ")
println(@allocated f_bertahap(a, b, c))

y = zeros(10_000)
y .= a .* b .+ c

# pemanasan jalur penugasan: perulangan broadcast! telah dikompilasi
print("in-place: ")
println(@allocated y .= a .* b .+ c)
text Keluaran
terfusi : 80072
bertahap: 160144
in-place: 64

Perulangan terfusi mengalokasikan 80072 byte untuk keluaran berisi 10000 bilangan Float64, sedangkan versi bertahap mengalokasikan dua kali lipatnya karena array sementara a .* b ikut tersimpan. Bentuk y .= a .* b .+ c mengalokasikan 64 byte saja, yaitu susunan objek Broadcasted-nya, karena seluruh hasil ditulis langsung ke array y yang sudah ada.

Makro @. membentuk gaya penulisan ini dengan meletakkan titik pada setiap operator dan pemanggilan fungsi dalam satu ekspresi. Dengan demikian, penugasan pada contoh berikut berarti z .= u .* v .+ w.

julia Contoh: makro @. meletakkan titik otomatis
u = [1.0, 2.0, 3.0]
v = [10.0, 20.0, 30.0]
w = [0.5, 0.5, 0.5]
z = zeros(3)
@. z = u * v + w
println(z)
text Keluaran
[10.5, 40.5, 90.5]

Kesalahan umum#

Tiga kesalahan berikut paling sering muncul saat topik ini dipraktikkan.

  • Menulis metode copyto!(dest, bc::Broadcasted{GayaSaya}) untuk tipe array sendiri tanpa menspesialisasikan tipe tujuannya. Metode itu ambigu terhadap metode bawaan copyto!(dest::AbstractArray, bc::Broadcasted), sehingga broadcast berhenti dengan MethodError. Spesialisasikanlah tipe tujuannya, misalnya copyto!(dest::TipeSaya, bc::Broadcasted{GayaSaya}).
  • Mengukur @allocated tanpa pemanasan. Pengukuran pertama menyertakan alokasi proses kompilasi sehingga angkanya membengkak. Jalankan ekspresi yang sama satu kali sebelum diukur.
  • Mengira penugasan broadcast bebas biaya. y .= ... memang tidak mengalokasikan array baru, tetapi tetap membaca dan menulis seluruh datanya. Penghematannya ada pada alokasi, bukan pada pekerjaan per elemen.

Ringkasan#

  • Ekspresi bertitik disusun malas sebagai pohon Broadcasted, dan materialize menghitungnya sekali jalan.
  • Gaya broadcast menentukan tipe keluaran dan implementasinya. Array bawaan memakai DefaultArrayStyle dan kalah dari gaya kustom yang berupa AbstractArrayStyle.
  • Gaya kustom membutuhkan tiga bagian, yaitu deklarasi BroadcastStyle, alokasi keluaran melalui similar, dan pelaksanaan melalui copyto!.
  • Fusing melebur seluruh operasi titik dalam satu ekspresi menjadi satu perulangan dengan satu alokasi keluaran, sedangkan .= menulis hasilnya ke array yang sudah ada.

Referensi#