ジュリアのブロードキャスティング文法
概要
ブロードキャスティングは Juliaで最も重要な概念の一つであり、ベクトル化されたコードを書く際に非常に便利な文法だ1。二項演算の前に.
を置いたり、関数の後に.
を置くことで使用する。これは点ごとに関数を適用するという意味であり、その目的にぴったりの表現だ。
プログラミング的にブロードキャスティングは、マップとリデュースのマップを使いやすくしたものと見ることができる。
コード
二項演算
二項演算には.
を付けて使用する。例えば、行列$A \in \mathbb{Z}_{9}^{3 \times 4}$の全要素にスカラ$a \in \mathbb{R}$を足すコードは以下の通りだ。
julia> A = rand(0:9, 3,4)
3×4 Matrix{Int64}:
5 6 3 3
7 4 8 8
0 2 2 7
julia> a = rand()
0.23234165065465284
julia> A .+ a
3×4 Matrix{Float64}:
5.23234 6.23234 3.23234 3.23234
7.23234 4.23234 8.23234 8.23234
0.232342 2.23234 2.23234 7.23234
一般関数
julia> f(x) = x^2 - 1
f (generic function with 3 methods)
julia> f(a)
-0.9460173573710713
例えば関数$f : \mathbb{R} \to \mathbb{R}$を考えてみよう。これはスカラ関数なので、$a \in \mathbb{R}$に対して上記のようにうまく計算される。
julia> f(A)
ERROR: LoadError: DimensionMismatch
しかし、行列$A$を入れてみるとLoadError
が発生する。考えてみれば、行列の平方、特に$A \in \mathbb{Z}_{9}^{3 \times 4}$のような直方体の行列の平方とは何か、という問題から始まる。そのため、$f(x) = x^{2} - 1$のような関数に無闇に入れることはできない。しかし、行列$A$の全ての値に対してそれぞれ平方を取り、その後$1$を引いた行列を得たい場合は、f.
のように点を打つことで、行列の全要素に関数$f : \mathbb{R} \to \mathbb{R}$を適用できる。
julia> f.(A)
3×4 Matrix{Int64}:
24 35 8 8
48 15 63 63
-1 3 3 48
速度比較
多くの場合、ブロードキャスティングは性能面でも優れている。しかし、速度を性能の基準として性能を評価する部分には、かなり難しい点があるので、必ず以下の内容を確認してほしい。
例として、以下は1から10万までの数に平方根を取るコードだ。
julia> @time for x in 1:100000
sqrt(x)
end
0.000001 seconds
julia> @time sqrt.(1:100000);
0.000583 seconds (2 allocations: 781.297 KiB)
単純な速度だけを比較すると、ブロードキャスティングはfor
ループよりも約500倍遅い。しかし、これは単純な計算から得られたベンチマークで、保存するプロセスまで含めた場合は話が変わる。
julia> z = []
Any[]
julia> @time for x in 1:100000
push!(z, sqrt(x))
end
0.005155 seconds (100.01 k allocations: 3.353 MiB)
julia> @time y = sqrt.(1:100000);
0.000448 seconds (2 allocations: 781.297 KiB)
保存するプロセスを含めても、ブロードキャスティングを適用したコードには変わりはないが、空の配列に値を追加しなければならない反復文の場合、ベクトル化されたコードと比べて約10倍遅いことがわかる。これはsqrt()
自体よりもpush!()
が動的配列を扱う際に消費するコストが大きいと言えるが、とにかく結果としてブロードキャスティング側が速い。当然、反復文をより速くする方法もあるが(例えばAny[]
がFloat64[]
に変わるだけで改善されるだろう)、実際に遭遇するほとんどのコーディングで、ブロードキャスティングを使用する方が扱いやすく、速度面でも優れている。
これは単なる概念的な部分を超え、ジュリアがインタープリターよりもコンパイラ言語に近い1こととも関連している。次のループで何が起こるかわからないfor
反復文よりも、タイプとサイズが具体的に決まっているベクトルに対してコンパイルする方が、コンパイラにとって楽ではないだろうか?
99%程度の関数では、私たちが独自に反復文を使うよりも、ジュリアを作った人たちが考案した方法をそのまま使う方が速いと断言できる。コードを無理にベクトル化する必要はないが、ベクトル化できるコードなら、ほとんどの場合、ベクトル化した方が圧倒的に…本当に圧倒的に速い。これはジュリアに限らず、マトラボ、Rのようにベクトル演算に特化した言語なら誰もが持っている特徴だが、関数型プログラミングのパラダイムを最もよく受け入れている新しい言語であり、速度面で自信を示している点が異なるだけだ。
環境
- OS: Windows
- julia: v1.7.0