Blog
ブログ

2024年12月19日

Julia言語を始めよう – Julia言語で高速にコードを実行する – SOHOBB Advent Calendar 2024

Julia言語の速さを実感する

Julia言語では、JITコンパイラといって実行する前にコードがコンパイルされるため、高速にコードを実行することができます。基本的に、コードを実行する際は関数化することによってより高速に実行することができます。簡単な二重ループなどは以下の様にして素早く実行することができます。

例として、Pythonではあまり良いとされない回数の多いforループは以下の様にして実装できます。また、@time を実行したい関数の前につけることで、簡単に時間を計測することができます。

Julia
function double_loop(N)
    s = 0
    for i = 1:N
        for j = 1:N
            s += i + j
        end
    end
    return s
end


N = 1000
@time double_loop(N)

これを実行すると、

Bash
  0.007823 seconds (6.64 k allocations: 329.688 KiB, 99.69% compilation time)

と実行にかかった時間及びメモリの使用量、コンパイルにかかった時間を簡単に確認できます。

とはいえ、N=1000程度ではあまり速さを実感しづらいため、もっと大きなNで実行させてみましょう。ちなみに、1度関数をコンパイルしてしまえば、次からは同じ関数で引数を変えて再利用する際はもっと速く実行することができます。

実際 N = 10,000,000としてみましょう。

Julia
@time double_loop(10_000_000)

# 0.000001 seconds

これをPythonで同様に実行すると大変ですね。Pythonはライブラリが豊富にあるため、単純な例であればライブラリの恩恵を受けることができますが、ライブラリに存在しないようなアルゴリズムを試してみたい場合は、スクラッチで実装するしかないためJulia言語が非常に便利な存在となります。

https://docs.julialang.org/en/v1/manual/performance-tips

こちらのサイトでは、Julia言語でパフォーマンスを上げるための様々なコツがあるため、こちらを見ていきましょう。とはいえ、ものすごくたくさんの量があるため、簡単に紹介できるものだけを中心に紹介していきます。

Julia言語の高速化Tips

型の指定されていないグローバル変数を避ける

型指定のないグローバル変数の利用はなるべく避けるか、事前に定数として定義しておきましょう。関数の引数として用いられる変数で、変更されないものはconstとして定数として定義しましょう。

Julia
const N = 10_000_000

function double_loop(N)
    s = 0
    for i = 1:N
        for j = 1:N
            s += i + j
        end
    end
    return s
end

@time double_loop(N)

抽象(Abstract)型の利用はなるべく避ける

配列を利用する際、要素の型がそれぞれ異なっているとパフォーマンスを下げる原因となります。なので、配列を定義するときに型を決めておいて後から代入するようにしましょう。

Julia
x = Float64[]

push!(x, 1.0)
push!(x, π)
push!(x, exp(1.0))

@show x

引数の型が異なる場合は関数を分けよう!

Julia言語には、「多重ディスパッチ」と呼ばれる。関数を引数の型ごとに分けて作成するということができます。シンプルな例ですが、「数字が引数の場合は単純に足し算する」「文字が引数の場合は文字列を結合する」といった処理は以下のようにして実装することができます。

Julia
function add(a::String, b::String)
    return a * b
end

function add(a::Number, b::Number)
    return a + b
end

@show add("Hello", "World")
@show add(1, 2)

基本的に同じ関数名を再定義するとエラーになりますが、このような場合ではエラーにならずに型に依存する場合分けとして実装することができます。これはアプリのバックエンドでも応用可能で、生年月日から年齢を計算するような関数にも柔軟な場合分けが実装かのうです。

Julia
function calc_age(birth_date::Date)
    today = Dates.today()
    return today.year - birth_date.year
end

function calc_age(birth_date::String)
    return calc_age(Dates.Date(birth_date, "yyyy-mm-dd"))
end

@show calc_age("1990-01-01")
@show calc_age(Dates.Date(1990, 1, 1))

素早く実行したい関数においてはこのようにして型を明確にしておくと良いでしょう。

勾配法を実装しようTips

勾配法とは、機械学習において行われる基本的なアルゴリズムで、関数の最小値を求めるメソッドとして利用されます。詳しく説明すると長くなりますが、簡単に説明すると「今自分のいる場所の傾きを考えて、傾きが一番急な向きに下っていけばよい」というアルゴリズムです。

参考 https://avilen.co.jp/personal/knowledge-article/optimizer/

Julia
using Plots
using LinearAlgebra

function gradient_method(f, ∇f, x0::Vector{Float64}, α::Float64, ϵ::Float64 = 1e-6)
    num_iter::Int = 1000
    x::Vector{Float64} = x0
    history::Vector{Float64} = []
    for i in 1:num_iter
        push!(history, f(x))
        x = x - α*∇f(x)
        if norm(∇f(x)) < ϵ
            break
        end
    end

    # 結果を表示
    println("Initial value: ", x0)
    
    # 最適解
    println("Optimal value: ", x)

    # 経過をプロット
    plot(history, xlabel="Iteration", ylabel="f(x)", label="f(x)", legend=:topleft)
    
end

function f(x::Vector{Float64})
    return (x[1]-3)^2 + (x[2]-5)^2
end

function ∇f(x::Vector{Float64})
    return [2*(x[1]-3), 2*(x[2]-5)]
end

@time gradient_method(f, ∇f, [0.0, 0.0], 0.1)

まず、グローバルな変数は基本的には使いません。思い切って関数の中に入れてしまいましょう。もし関数で変数を渡したりしたいのであればmain関数などを定義しましょう。また、型が決まっているような変数は積極的に型を定義しておきましょう。print文などは基本的にはループの中では避けるといった基本的なコーディングも重要ですね。

簡単な2次関数ですので、以下のように(3,5)

Julia
Initial value: [0.0, 0.0]
Optimal value: [2.9999997472504996, 4.999999578750833]
  0.104335 seconds (43.10 k allocations: 2.103 MiB, 95.44% compilation time: 16% of which was recompilation)

に収束することが確認できます。

おわりに

今回はJulia言語の紹介ということで、簡単な例を紹介するにとどめましたが、今後はJulia言語を用いて複雑な機械学習の計算や画像処理、多言語との連携なども取り入れて行きたいと思います。

このページの先頭へ