# Juliaの基本機能の処理速度をPython3のそれと比較する

Juliaは非常に高速な処理系を持つプログラミング言語であることは知られていて、比較的新しい言語です。

特にJuliaが科学技術分野で使われることが多く、Juliaと同じように動的型付けで科学技術分野でよく利用されるPython3と比較されることが多いと思っています。

私としては普段Python3を書いていて、その開発体験をJuliaと比較するとfor文を回すならJuliaの方が早く、それ以外ではJuliaの初回実行のoverheadが気がかりになっていました。

そこで、Python3とJuliaの速度比較をし、それぞれの良い点を探してみようと思います。

# 環境

  • OS: macOS Catalina
  • CPU: Intel(R) Core(TM) i5-8257U CPU @ 1.40GHz

# 処理速度の比較

上記環境において付録に記載したソースコードを実行し、処理速度を観察します。

処理速度は1000回試行の平均値を取っています。 初回実行は以下の通りです。

julia python 3
1次元配列生成 1.8993666999999997e-5 1.0246700000000192e-07
2次元配列生成 3.5582449e-5 5.938880000000008e-07
配列要素の探索 5.7263490000000004e-6 4.14549999999958e-08
for ループ 6.618205e-6 0.0024388290569999998
足し算 4.984889e-6 4.75229999996607e-08
引き算 5.010657e-6 6.145899999987492e-08
掛け算 6.175515000000001e-6 8.62829999999093e-08
割り算 9.366116e-6 4.331600000018199e-08
剰余 6.291158e-6 4.417400000011895e-08

for ループは Python 3 と比べて約 368.5 倍、Julia の方が速いという結果になりました。それ以外は50~200倍程Python3の方が速いです。これはJuliaの初回実行が遅い問題(latency)の影響であることが予想できます。

JITコンパイラがすでに実行された状態での速度を調べるために、同一スクリプト内でもう一度実行します。 それによりすでにコンパイルされている状態の関数を実行することができます。

Julia Python 3.x
1次元配列生成 3.7437e-8 7.039099999994747e-08
2次元配列生成 6.5323e-8 3.5948000000018967e-07
1次元配列要素の探索 2.273e-9 4.344300000003187e-08
for ループ 1.6840000000000002e-9 0.002422414534
足し算 2.0889999999999997e-9 6.589799999989765e-08
引き算 2.326e-9 6.680100000000522e-08
掛け算 4.619e-9 1.03358999999692e-07
割り算 3.38e-9 6.455899999924241e-08
剰余 4.7469999999999994e-9 4.539599999997534e-08

結果は上の通りで、全てのケースにおいてJuliaの実行速度が高速となっています。

以下の図はJuliaの1回目の実行と2回目の実行のそれぞれの実行速度を比較した図です。

すなわち, 気軽に小さなプログラムを書いて実行する場合はPython3の方が若干早くて有利な点がありますが、Forループなどを多様する多少実行速度が影響するレベルの規模のプログラムの場合、Juliaで書いた方が良いということになります。

# Juliaの特徴

Juliaの特徴として、global変数を使うと遅くなり、関数の中に閉じた処理を実行すると速い、という性質があります。 これはおそらく関数として処理をまとめているとその部分の実行は1度で済み、script内で再利用するときにoverheadがないという点からだと思いますが、なぜそうなのかを見てみます。


足し算のサンプルプログラム

using InteractiveUtils

function add()
    t = 10
    t += 2
end

@code_llvm add()

上記スクリプトの中間コード

;  @ /Users/UserName/AIIT/src/kadai2/analysis.jl:3 within `add'
define i64 @julia_add_48() {
top:
;  @ /Users/UserName/AIIT/src/kadai2/analysis.jl:5 within `add'
  ret i64 12
}

グローバル変数を用いた足し算のスクリプト

using InteractiveUtils
t = 10
function add_global()
    global t += 2
end
@code_llvm add_global()
上記スクリプトの中間コード(長いので畳みます) ```llvm ; @ /Users/UserName/AIIT/src/kadai2/analysis.jl:15 within `add_global' define nonnull %jl_value_t* @julia_add_global_50() { top: %0 = alloca %jl_value_t*, i32 2 %gcframe = alloca %jl_value_t*, i32 3, align 16 %1 = bitcast %jl_value_t** %gcframe to i8* call void @llvm.memset.p0i8.i32(i8* align 16 %1, i8 0, i32 24, i1 false) %2 = call %jl_value_t*** inttoptr (i64 4527529104 to %jl_value_t*** ()*)() #4 ; @ /Users/UserName/AIIT/src/kadai2/analysis.jl:16 within `add_global' %3 = getelementptr %jl_value_t*, %jl_value_t** %gcframe, i32 0 %4 = bitcast %jl_value_t** %3 to i64* store i64 4, i64* %4 %5 = getelementptr %jl_value_t**, %jl_value_t*** %2, i32 0 %6 = load %jl_value_t**, %jl_value_t*** %5 %7 = getelementptr %jl_value_t*, %jl_value_t** %gcframe, i32 1 %8 = bitcast %jl_value_t** %7 to %jl_value_t*** store %jl_value_t** %6, %jl_value_t*** %8 %9 = bitcast %jl_value_t*** %5 to %jl_value_t*** store %jl_value_t** %gcframe, %jl_value_t*** %9 %10 = load %jl_value_t*, %jl_value_t** inttoptr (i64 4781914632 to %jl_value_t**), align 8 %11 = getelementptr %jl_value_t*, %jl_value_t** %gcframe, i32 2 store %jl_value_t* %10, %jl_value_t** %11 %12 = getelementptr %jl_value_t*, %jl_value_t** %0, i32 0 store %jl_value_t* %10, %jl_value_t** %12 %13 = getelementptr %jl_value_t*, %jl_value_t** %0, i32 1 store %jl_value_t* inttoptr (i64 4749885600 to %jl_value_t*), %jl_value_t** %13 %14 = call nonnull %jl_value_t* @jl_apply_generic(%jl_value_t* inttoptr (i64 4645998480 to %jl_value_t*), %jl_value_t** %0, i32 2) %15 = getelementptr %jl_value_t*, %jl_value_t** %gcframe, i32 2 store %jl_value_t* %14, %jl_value_t** %15 call void @jl_checked_assignment(%jl_value_t* nonnull inttoptr (i64 4781914624 to %jl_value_t*), %jl_value_t* %14) %16 = getelementptr %jl_value_t*, %jl_value_t** %gcframe, i32 1 %17 = load %jl_value_t*, %jl_value_t** %16 %18 = getelementptr %jl_value_t**, %jl_value_t*** %2, i32 0 %19 = bitcast %jl_value_t*** %18 to %jl_value_t** store %jl_value_t* %17, %jl_value_t** %19 ret %jl_value_t* %14 } ```

中間コードを見てみると、グローバル変数を用いた方はとても複雑な中間コードが生成されました。 速度は計測していませんが、おそらく非効率であり、Juliaではglobal変数は使わない方が良いのだとおもいます。

# 付録

Juliaの基本機能のソースコード
using BenchmarkTools

# https://stackoverflow.com/questions/61171531/difference-between-array-and-vector

function array(ITR_NUM)
    ary = nothing
    for i=1:ITR_NUM
        ary = Array(1:2:5)
    end
    ary
end

function multi_array(ITR_NUM)
    ary = nothing
    for i=1:ITR_NUM
        ary = [1 2 3 4 5;1 2 3 4 5]
    end
    ary
end

function find_elem(ITR_NUM, ary, p)
    for i=1:ITR_NUM
        ary[p]
    end
end

function for_loop(ITR_NUM)
    for i=1:ITR_NUM
        for t = 1:100000
            t
        end
    end
end

function summation(ITR_NUM)
    p = 2
    for i=1:ITR_NUM
        p += 1
    end
    p
end

function difference(ITR_NUM)
    p = 2
    for i=1:ITR_NUM
        p -= 1
    end
    p
end

function product(ITR_NUM)
    p = 1
    for i=1:ITR_NUM
        p *= 2
    end
    p
end

# 割り算
function quotient(ITR_NUM)
    p = 200000000
    for i=1:ITR_NUM
        p /= 2
    end
    p
end

# 剰余
function remainder(ITR_NUM)
    p = 200000001
    for i=1:ITR_NUM
        p %= 17
    end
    p
end

function main()
    # runtime = @elapsed begin
    ITR_NUM = 1000
    result = runtime = nothing
    # result, runtime = @timed vector(ITR_NUM)
    # println("vector: ", result, ", ", runtime/ITR_NUM)
    result, runtime = @timed array(ITR_NUM)
    println("array: ", result, ", ", runtime/ITR_NUM)

    result, runtime = @timed multi_array(ITR_NUM)
    println("multi_array: ", result, ", ", runtime/ITR_NUM)

    # 要素は5つなので大丈夫。
    ary = Array(1:2:5)
    result, runtime = @timed find_elem(ITR_NUM, ary, 2)
    println("find_elem: ", result, ", ", runtime/ITR_NUM)

    # for文
    result, runtime = @timed for_loop(ITR_NUM)
    println("for_loop: ", result, ", ", runtime/ITR_NUM)

    result, runtime = @timed summation(ITR_NUM)
    println("足し算 summation: ", result, ", ", runtime/ITR_NUM)
    result, runtime = @timed difference(ITR_NUM)
    println("引き算 difference: ", result, ", ", runtime/ITR_NUM)
    result, runtime = @timed product(ITR_NUM)
    println("掛け算 product: ", result, ", ", runtime/ITR_NUM)
    result, runtime = @timed quotient(ITR_NUM)
    println("割り算 quotient: ", result, ", ", runtime/ITR_NUM)
    result, runtime = @timed remainder(ITR_NUM)
    println("剰余 remainder: ", result, ", ", runtime/ITR_NUM)
end

# 初回実行
main()
# 2回目実行
println("===============2回目===============")
main()

Python3の基本機能のソースコード
import random, time

def vector(ITR_NUM):
    vec = None
    for _ in range(ITR_NUM):
        vec = [1,2,3,4,5]
    return vec

def multi_vector(ITR_NUM):
    vec = []
    for _ in range(ITR_NUM):
        vec.append([1,2,3,4,5])
        vec.append([1,2,3,4,5])
    return vec

def find_elem(ITR_NUM, ary, p):
    for _ in range(ITR_NUM):
        ary[p]


def for_loop(ITR_NUM):
    for _ in range(ITR_NUM):
        for i in range(100000):
            i



def summation(ITR_NUM):
    p = 2
    for _ in range(ITR_NUM):
        p += 1
    return p


def difference(ITR_NUM):
    p = 20000
    for _ in range(ITR_NUM):
        p -= 2
    return p


def product(ITR_NUM):
    p = 1
    for _ in range(ITR_NUM):
        p *= 2
    return p

def quotient(ITR_NUM):
    p = 200000000
    for _ in range(ITR_NUM):
        p /= 2
    return p

# 剰余
def remainder(ITR_NUM):
    p = 200000001
    for _ in range(ITR_NUM):
        p %= 17
    return p

def main():
    ITR_NUM = 1000
    before = time.perf_counter()
    vector(ITR_NUM)
    print("1次元配列 {}".format((time.perf_counter() - before)/ITR_NUM))
    
    before = time.perf_counter()
    multi_vector(ITR_NUM)
    print("2次元配列 {}".format((time.perf_counter() - before)/ITR_NUM))


    ary = [1,2,3,4,5]
    p = 2
    before = time.perf_counter()
    find_elem(ITR_NUM, ary, p)
    print("配列の探索 {}".format((time.perf_counter() - before)/ITR_NUM))

    before = time.perf_counter()
    for_loop(ITR_NUM)
    print("for ループ100000回 {}".format((time.perf_counter() - before)/ITR_NUM))

    before = time.perf_counter()
    summation(ITR_NUM)
    print("足し算 {}".format((time.perf_counter() - before)/ITR_NUM))

    before = time.perf_counter()
    difference(ITR_NUM)
    print("引き算 {}".format((time.perf_counter() - before)/ITR_NUM))

    before = time.perf_counter()
    product(ITR_NUM)
    print("掛け算 {}".format((time.perf_counter() - before)/ITR_NUM))

    before = time.perf_counter()
    quotient(ITR_NUM)
    print("割り算 {}".format((time.perf_counter() - before)/ITR_NUM))

    before = time.perf_counter()
    remainder(ITR_NUM)
    print("剰余 {}".format((time.perf_counter() - before)/ITR_NUM))


if __name__ == "__main__":
    print(" ===== CPython 3.x 計測 ===== ")
    main()

    print(" ===== CPython 3.x 2回目 計測 ===== ")
    main()

Last Updated: 7/12/2022, 8:19:49 PM