julia の Array の vcat() を見直して、組み上げ速度を上げよう
はじめに
julia 0.6.3 で、計算しながら Array を組み上げると、ちょっと時間がかかることがあります。 vcat() と、空の Array への代入はどちらが、早いのでしょうか?
下記の実験で判ったのは、
- Array の中に文字列があると、Array の結合は、大幅に速度が落ちます
- Array を 数回、 vcat() で結合するなら、 vcat() の実用性は高いです
- 文字列ありで、100回程度以上、 vcat() するなら、先に入れ物の Array を作り、代入する方法を検討しましょう
ということです。
数字だけの2つの Array を積み重ねてみよう
まず、A と B という 10^4 × 3 の Array を作ります。
A = hcat( randn(10^4), randn(10^4), randn(10^4) ); B = hcat( randn(10^4), randn(10^4), randn(10^4) );
まずは、vcat() で、A と B を積み重ねます。5回、時間を測定してみましょう。
julia> for i in 1:5 @time C_vcat = vcat(A,B); end 0.125543 seconds (55.42 k allocations: 3.453 MiB) 0.000096 seconds (9 allocations: 468.984 KiB) 0.000380 seconds (9 allocations: 468.984 KiB) 0.000337 seconds (9 allocations: 468.984 KiB) 0.000291 seconds (9 allocations: 468.984 KiB)
MacBook Pro が、電池駆動中のためか、少し時間にブレが出ています。
次に、先に入れ物 Array を、Any の型で、組んで代入する関数を作ります。
function Tum_Tum(A::Array,B::Array) size_A = size(A)[1]; C = Array{Any}(size_A+size(B)[1],3); C[1:size_A,:] = A C[(size_A+1):end,:] = B return C end
では、測定してみましょう。
julia> for i in 1:5 @time Tum_Tum(A,B) end 0.011303 seconds (63.55 k allocations: 1.535 MiB) 0.000452 seconds (60.01 k allocations: 1.373 MiB) 0.000619 seconds (60.01 k allocations: 1.373 MiB) 0.001146 seconds (60.01 k allocations: 1.373 MiB) 0.001165 seconds (60.01 k allocations: 1.373 MiB)
上記だと、さすがに vcat の方が早いです。
念のため、同じものができているか確かめてみましょう。
julia> C_vcat == Tum_Tum(A,B) true
大丈夫でした。私は、10001 行目で結合で良いか? なんて、ちょっぴり不安になります。
では、ここで、Any だった型を、Float64 に変更したら、どうなるでしょう?
function Tum_Tum_Float(A::Array,B::Array) size_A = size(A)[1]; C = Array{Float64}(size_A+size(B)[1],3); C[1:size_A,:] = A C[(size_A+1):end,:] = B return C end
では、測定です。
julia> for i in 1:5 @time Tum_Tum_Float(A,B) end 0.012903 seconds (3.43 k allocations: 627.708 KiB) 0.000391 seconds (6 allocations: 468.922 KiB) 0.000387 seconds (6 allocations: 468.922 KiB) 0.000654 seconds (6 allocations: 468.922 KiB) 0.000585 seconds (6 allocations: 468.922 KiB)
これは良い感じですね。Array の型を指定するのはとても大事ですね。
文字列を含む2つの Array を積み重ねてみよう
ここで、懲りずに、今度は、string 入りの Array で試してみます。
A2 = hcat( randn(10^4), randn(10^4), "string_".* string.(collect(1:10^4)) ); B2 = hcat( randn(10^4), randn(10^4), "string_B_".* string.(collect(1:10^4)) );
まずは、vcat() で、測定しましょう。もちろん、vcat(A2,B2) で動きます。
julia> for i in 1:5 @time = vcat(A2,B2); end 0.211822 seconds (30.92 k allocations: 2.121 MiB) 0.140495 seconds (9 allocations: 468.984 KiB) 0.145904 seconds (9 allocations: 468.984 KiB) 0.142249 seconds (9 allocations: 468.984 KiB) 0.139703 seconds (9 allocations: 468.984 KiB)
なんと、string が混った Array の結合は、vcat() は、かなりてこずるようです。
では、先に Array の型を Any で指定した Array に代入してみましょう。
この場合は、先程の Tum_Tum() が使えますね。
julia> for i in 1:5 @time Tum_Tum(A2,B2) end 0.144137 seconds (3.55 k allocations: 634.036 KiB) 0.137178 seconds (6 allocations: 468.922 KiB) 0.140681 seconds (6 allocations: 468.922 KiB) 0.144750 seconds (6 allocations: 468.922 KiB) 0.140838 seconds (6 allocations: 468.922 KiB)
なんと、自作 Tum_Tum() がんばっています。
もちろん、
julia> vcat(A2,B2) == Tum_Tum(A2,B2) true
答えは一緒ですね。
では、入れ物の Array の型を正確に指定してみましょう。
まず、Array{String}(size_A+size(B)[1]) が、Float64 型の隣りに入らずに苦戦します。
julie> C = hcat( Array{Float64}(size_A+size(B)[1]), Array{Float64}(size_A+size(B)[1]), Array{String}(size_A+size(B)[1]) ); ERROR: UndefRefError: access to undefined reference
仕方が無いので、 "" を先に代入して組みます。
function Tum_Tum_FFS(A::Array,B::Array) size_A = size(A)[1]; C_3 = Array{String}(size_A+size(B)[1]); for i in 1:length(C_3);C_3[i] = "";end; C = hcat( Array{Float64}(size_A+size(B)[1]), Array{Float64}(size_A+size(B)[1]), C_3 ); C[1:size_A,:] = A C[(size_A+1):end,:] = B return C end
これは組めました。
では、測定してみましょう。
julia> for i in 1:5 @time Tum_Tum_FFS(A2,B2) end 0.187246 seconds (68.13 k allocations: 2.961 MiB) 0.143167 seconds (40.02 k allocations: 1.526 MiB) 0.140169 seconds (40.02 k allocations: 1.526 MiB) 0.140350 seconds (40.02 k allocations: 1.526 MiB) 0.155728 seconds (40.02 k allocations: 1.526 MiB, 8.37% gc time)
vcat() を越えるかも? と夢を見ましたが、さすがに、vcat() は越えられません。
やはり、string 型の列を作るあたりで、余計な作業なのでしょう。
どうしても、1行ずつ Array を作らないといけない場合はどうだろう
ここで、もう少し、特殊な場合を考えます。
どうしても 1 行ずつ計算をして求めないといけない場合はどうでしょう。
実数 x を、計算式に入れて f(x) を求めて、note という文字列を付けることにします。
[x, f(x), note]
という行を、次々に積み重ねていくことにします。
今回は複雑な計算は思いつきませんので、f(x) = sin(x) としてみることにします。
では、まず、vcat() ばかりの関数の定義をします。
function vcat_test(n::Int64) array_ans = [] array_ans = hcat(1, sin(1), "hoge_1") for i in 2:n array_ans = vcat(array_ans , hcat(i, sin(i), "hoge_"*string(i))) end return array_ans end
測定してみましょう。
julia> for i in 1:5 @time vcat_test(100) end 0.040969 seconds (7.19 k allocations: 381.453 KiB) 0.044292 seconds (7.19 k allocations: 381.453 KiB) 0.038721 seconds (7.19 k allocations: 381.453 KiB) 0.037588 seconds (7.19 k allocations: 381.453 KiB) 0.037962 seconds (7.19 k allocations: 381.453 KiB)
次に先に、入れものを作っておく、関数の定義です。
function Substitution_test(n::Int64) array_ans = Array{Any}(n,3) for i in 1:n array_ans[i,:] = hcat(i, sin(i), "hoge_"*string(i)) end return array_ans end
測定してみましょう。
julia> for i in 1:5 @time Substitution_test(100) end 0.003589 seconds (6.70 k allocations: 241.563 KiB) 0.002266 seconds (6.70 k allocations: 241.563 KiB) 0.001659 seconds (6.70 k allocations: 241.563 KiB) 0.001627 seconds (6.70 k allocations: 241.563 KiB) 0.001628 seconds (6.70 k allocations: 241.563 KiB)
100回くらいくりかえすと、1桁の差が出てきました。
なるべく入れものを先にした方が、仮に {Any} の型でも良いようですね。
文字列だけ、別の Array にしたら、もっと速いよ、なんて、声が聞こえてきそうです。