MyEnigma

とある自律移動システムエンジニアのブログです。#Robotics #Programing #C++ #Python #MATLAB #Vim #Mathematics #Book #Movie #Traveling #Mac #iPhone

Juliaのコードを更に高速化する方法

目次

はじめに

以前、

MATLABのコードの高速化手法や

myenigma.hatenablog.com

Pythonコードの高速化の手法を紹介しましたが、

myenigma.hatenablog.com

myenigma.hatenablog.com

myenigma.hatenablog.com

myenigma.hatenablog.com

 

今回は、下記の公式記事を元に、

Juliaのコードを高速化する方法をまとめたいと思います。

docs.julialang.org

 

Juliaは普通に使用しても十分高速ですが、

下記のTipsを利用することで、

より高速化することができます。

 

Juliaという言語そのものに関しては、

下記の記事を参照ください。

myenigma.hatenablog.com

  

Juliaを高速化するために注意すべきこと

下記はJuliaのコードにおいて、

処理を高速化するために注意すべきことをまとめたものです。

 

グローバル変数を避ける

グローバル変数は、数値を保持することが可能ですが、

そのグローバル変数の型や値は、

任意のタイミングで変更される可能性があります。

つまり、コンパイラとしては、

そのコードを最適化するのが難しくなってしまいます。

どんな時でもできるだけ、

すべての変数はローカルか、

または関数の引数として与えられるべきです。

 

またすべてのパフォーマンスが重要で、

ベンチマークを取っているコードは

関数の中に書かれるべきです。

 

一般的に、グローバル変数は定数値として利用されるため、

もしグローバル変数をJuliaで使う場合、下記のようにconst修飾子を使うと、性能が向上します。

const DEFAULT_VAL = 0

 

定数でないグローバル変数を使う場合、

そのグローバル変数に型情報を追加することで、

コードを最適化することができます。

global x
y= f(x::Int + 1)

しかし、グローバル変数を使わずに、

関数の入力と出力のみに依存するようなコードを書くことは、

コードのパフォーマンスだけでなく、

コードの可読性や再利用性を向上させることができます。

 

@timeでパフォーマンスを計測し、メモリアロケーションに注意を払うこと

Juliaでは、コードのパフォーマンスを計測するのに、

便利なマクロである@timeがあります。

 

このマクロでは関数の前に@timeとすると、

コード実行時に、その関数の計算時間とメモリアロケーションを表示できます。

 

もし使用メモリが想像よりも大きい場合、

そのコードは型の選択等にミスがある可能性が多く、

コードを改良できる可能性が多いです。

使用メモリを減らすと、計算時間も短くなることが多いです。

 

もし、より精密なパフォーマンスを計測したい場合は、

BenchmarkTools.jl を使うことができます。

github.com

このツールは関数を複数回呼んで、

より精密な計算結果を得ることができます。

 

パフォーマンス関連ツールを使う

Juliaには、コードの問題を解析したり、

コードを改善してくれるツール群があります。

これらを積極的に使用しましょう。

 

一つ目はProfilingマクロです。

https://docs.julialang.org/en/stable/manual/profile/#Profiling-1

これを使うことでコードのパフォーマンスを簡単に計測したり、

コードのボトルネックを簡単に計測することができます。

 

もしより複雑なプロジェクトをプロファイリングしたい場合は、

Profile Viewパッケージを使いましょう。

コードのボトルネックを可視化することができます。

github.com

 

もし、意図しないような巨大なメモリアロケーションが発生していることを、

@timeや@allocatedで発見した場合、

あなたのコードには問題がある場合が多いです。

もし問題を発見できなかった場合、

—track-allocation=user オプションを使ってJuliaを実行し、

*.memファイルを確認してください。

こちらの手法に関しては、下記を参照ください。

https://docs.julialang.org/en/stable/manual/profile/#Memory-allocation-analysis-1

 

こちらのLintパッケージを使うことで、

いくつかの種類のプログラミングの問題を自動解析することができます。

github.com

 

抽象型のコンテナは避ける

配列のような型をついう時は、

できるだけ、抽象的な型は避けた方が良いです。

例えば、下記のようなJuliaのコードがあったとします。

A = Real[]
If (f = rand())< .8
    push!(a, f)
end

 

しかし、上記のコードではaは必ずFloat64になるので、

a = Float64[]として初期化したほうが良いです。

なぜなら、Realの型はどんなReadなオブジェクトでも格納できるように、

配列が確保されるからです。

詳細は下記を参照ください。

 

型宣言

多くの言語において、

型宣言はオプションであり、

型宣言を追加することは、

コードを高速化する上で非常に重要です。

しかし、Juliaでは少し違います。

Juliaでは型情報はすべて、

コンパイラが認識するものだからです。

しかし、いくつかの特別な状況において、

型宣言は有効なものになります。

 

抽象型のフィードを避ける

下記のようなコードで、

特に型を指定せずにフィールドを指定できます。

struct MyAmbiguousTyoe
    a
end

このaというフィールドはどのような型にもなれます。

これはしばしば有効ですが、問題もあります。

このMyAmbiguousTypeのオブジェクトに関しては、

コンパイラはパフォーマンスの良いマシンコードを生成できないのです。

なぜなら、コンパイラは変数の型を決定する際に、

この型では変数の値ではなく、

使われ方によって型を決定する必要があるからです。

つまり、型の決定はランタイムで実行する必要があるため、

コードのパフォーマンスを低下させてしまいます。

 

この問題を解決するには、

フィールドに型宣言する方法があります。

struct MyTyoe
    a::Float64
end

 

キーワード引数の型を宣言する

キーワード引数に対しても 下記のように型を指定できます。

function with_keyword(x; name::Int = 1)
    ...
end

上記のように関数の引数の型を指定すると、

関数の中のパフォーマンスは変わりませんが、

関数の呼び出しのパフォーマンスは向上します。

ちなみに、動的リストを引数とすると、

コードのパフォーマンスが悪くなりがちなので、

パフォーマンスが重要なコードには注意が必要です。  

関数を複数の定義に分ける

複数の小さな処理で構成される関数は、

コンパイラによるインライン化が可能であるため、

パフォーマンスが向上する可能性があります。   例えば、下記のような引数の型に応じて、

処理を変化させる関数を書いた場合、

function norm(A)
    if isa(A, Vector) 
        return sqrt(real(dot(A,A))) 
    elseif isa(A, Matrix) 
        return maximum(svd(A)[2]) 
    else 
        error("norm: invalid argument")
    end
end

上記のようなコードは、

下記のように複数の細かい定義に分けたほうが

性能が高くなります。

norm(x::Vector) = sqrt(real(dot(x,x)))
norm(A::Matrix) = maximum(svd(A)[2])

 

型安定なコードを書く

もし可能であれば、

関数は常に同じ型の値を返すようにするべきです。

例えば、下記の関数関数を考えてみます。

pos(x) = x < 0 ? 0 : x

この関数の問題点は、

この関数は0というIntを返す可能性がある場合と、

Xという任意の型の値を返す可能性があるということです。

これは関数のパフォーマンスを低下させる可能性があります。

これは下記のようなコードにすることで、

型安定な関数にすることができます。

pos(x) = x < 0 ? zero(x) : x

上記のzeroという型は、

xの型に応じて、0を意味する値を返してくれます。

Juliaにはzero()以外にも、one()やoftype(x, y)という関数が準備されており、

これらを使うことで型安定な関数にすることができます。

 

変数の型が変化しないようにする

ある変数の中で、同じ変数を複数回利用すると、

型安全の問題が発生しやすくなります。

 

例えば、下記のような関数において、

function foo()
    x = 1
 for i = 1:10
        x = x / bar()
   end
end

変数xは初め整数値として初期化されますが、

その後ループの中では/演算子より、floatの型になります。

このようなコードはコンパイラはループの中を

最適化するのが非常に難しくなります。

 

このコードを型安全にするには、下記のような方法があります。

  • x を x = 1.0として初期化する。

  • xの型を明示的にfloatnにする。: x::Float64 = 1

  • oneunitという明示的な型変換を使う。

  

コア関数を分ける

多くの関数は、変数のセットアップをしたあと、

それらの変数を使って、

複数の繰り返し計算をします。

このような場合、

もし可能であれば、コアとなる関数を分離した方が良いです。   例えば、下記のような関数があるとします。

function strange_two(n)
    A = Vector{rand(Bool) ? Int64:Float64}(n)
    for i = 1:n
        a[i] = 2
    end   
    return a
end   

このコードは引数の次元分のすべての要素が2である

配列を返す関数ですが、

もし可能であれば、

下記のように書いた方が良いです。

function fill_twos!(a)
 for i=1:length(a)
      a[i] = 2
    end
end

function strange_two(n)
    A = Vector{rand(Bool) ? Int64:Float64}(n)
    fill_twos!(a)
    return a
end   

   

なぜならJuliaのコンパイラは、

変数の型を関数毎に評価しているからです。

一つ目の実装では、コンパイラはaの型がわからないため、

その下のループのコードを最適化することができません。

しかし、二つ目のバージョンはfill_twos関数は、

それぞれ別の型の入力毎に再コンパイルされるため、

より高速なコードが生成される可能性があります。

 

この手法はJulaの標準ライブラリでも、

しばしば利用されています。 

(abstractarray.jlのhvcat_fill関数など)

 

配列はメモリの順番に列ベースでアクセスする

Juliaでは多次元の配列は、

列ベースで格納されています。

つまり、列ベースでの一次元配列には

[:]の演算子を使うことで、簡単に変換できます。

 

julia> x = [1 2; 3 4] 2×2 Array{Int64,2}: 1 2 3 4

julia> x[:] 4-element Array{Int64,1}: 1 3 2 4

 

この列ベースのメモリ格納は、

FortranやMatlab, Rなどで採用されていますが、

CやPythonでは行ベースが採用されています。

これは、行列のスライスなどを多様する時に、

プログラムのパフォーマンスに大きな影響を与えます。

 

例えば、ループ中では下記のように、

下記のように、行ベースでデータを格納するよりも、

function copy_cols(x::Vector{T}) where T
    n = size(x, 1)
    out = Array{T}(n, n)
    for i = 1:n
        out[i, :] = x
    end
    out
end

列ベースでデータを追加したほうが、

計算が高速です。

function copy_cols(x::Vector{T}) where T
    n = size(x, 1)
    out = Array{T}(n, n)
    for i = 1:n
        out[:, i] = x
    end
    out
end

 

返り値を事前にメモリ割り当てする

あなたの関数が、Arrayやその他の複雑な型の

データを返す場合、

事前にメモリを割り当てておくと良い場合があります。

 

例えば、下記のようなリストを返す関数がある場合、

下記のようにループで関数を呼ぶ場合は、

毎回、変数のメモリ割当てが発生してしまいます。

function xinc(x)
    return [x, x+1, x+2]
end

function loopinc()
    y = 0
    for i = 1:10^7
        ret = xinc(i)
        y += ret[2]
    end
    y
end

そのような場合、

下記のように、事前に出力のデータを生成し、

その後毎回関数の中で、中身を変更するだけにすると、

計算パフォーマンスを向上させることができます。

function xinc!(ret::AbstractVector{T}, x::T) where T
    ret[1] = x
    ret[2] = x+1
    ret[3] = x+2
    nothing
end

function loopinc_prealloc()
    ret = Array{Int}(3)
    y = 0
    for i = 1:10^7
        xinc!(ret, i)
        y += ret[2]
    end
    y
end

しかしこの方法はコードを汚くしがちなので、

パフォーマンス計測を使って、

この手法を利用すべきかを検討したほうが良いでしょう。

 

ドット演算子を使う

Juliaには、ドット演算子という特別な演算子があります。

これは、任意のスカラー関数を、

ベクトル関数に変更したり、

任意のスカラー演算子をベクトル演算子に変えるものです。

このベクトル演算子をうまく使うことで、

無駄な一時変数のアロケーションを減らすことができ、

結果的に計算速度を向上させることができます。

 

スライスのViewを使うことを検討する

Juliaでは、配列のスライスは配列のコピーを作ってしまいます。

(左辺にスライスがある場合はコピーは作られません。)

これは大量のスライスを処理する場合は、

配列のコピーを作ったほうが良いですが、

少しのスライスの処理であれば、

インデックスのみを保存した方が計算性能が高いでしょう。

そこで便利なのは、配列のViewです。

これは配列の各要素の参照をあつめた配列データです。

このViewを作る場合は、配列のコピーは生成されません。

(その代わり、元の入れるの中身を修正すると、

Viewの中身も変化します。)

viewを作る場合は、view()関数を使うか、

@viewsマクロを使うとviewを作ることができます。

 

I/Oに対する文字列補間を避ける

ファイルへのデータ書き込みなど、

I/Oに対する出力では、文字列補間は避けたほうが良いです。

つまり、下記のようなコードではなく、

println(file, "$a $b")

下記のように書くべきです。

println(file, a,  " ", b)

Depreciation Warningを修正する

すでにDeprecateされた関数や機能を使うと、

JuliaではDeprecation Warningが発報されますが、

このWarningは、大量に発生しないように制御されています。

しかし、この処理は計算リソースを消費する処理なので、

できるだけDeprecation Warningは修正するようにしましょう。

 

細かいTips

いくつか細かい計算パフォーマンス向上のための

tipsがあります。

  • 不必要な配列を使わない (sum([x,y,z] よりx+y+zを使う)

  • abs(z)2よりabs2(z)を使う

  • trunc(x/y)より、div(x,y)を使う

  • floor(x/y)より、fld(x,y)を使う

  • ceil(x/y)よりcld(x,y)を使う

 

パフォーマンス向上のためのマクロ

いくつかのマクロを使うことで、

コードを高速化することができます。

  • @inbounds 配列の境界チェックをしなくします。コードは高速になりますが、配列がオーバフローすると急にコードがクラッシュしたりします。

  • @fastmath 浮動小数点の計算をさいt経過しますが、IEEEの数値計算とは若干計算結果が異なります。clangの-ffast-mathオプションと同じ機能です。

  • @simd forループの前に置くと、ベクター処理されます。しかし、この機能はexperimentalのようです。

 

参考資料

myenigma.hatenablog.com

myenigma.hatenablog.com

myenigma.hatenablog.com

myenigma.hatenablog.com

myenigma.hatenablog.com

 

MyEnigma Supporters

もしこの記事が参考になり、

ブログをサポートしたいと思われた方は、

こちらからよろしくお願いします。

myenigma.hatenablog.com