技術は使ってなんぼ

自分が得たものを誰かの役に立てたい

【Python】Numba(JIT)を使った高速化は内包表記よりもfor文が速い件

Numba(JIT)を使った高速化

以下は手軽に試せるテストコードを記載。

import time
from numba import njit, jit, prange

# global
roop_num = 10000000

#@njit
@jit(parallel=True)
def func_app(roop):
    list_a = []
    for i in range(roop):
        list_a.append(i)

start_time = time.time()
func_app(roop_num)
end_time = time.time() - start_time
print("time : {}".format(end_time), "[sec]")


内包表記の場合は以下。

import time
from numba import njit, jit, prange

# global
roop_num = 10000000

#@njit
@jit(parallel=True)
def func_app(roop):
    [i for i in range(roop)]

start_time = time.time()
func_app(roop_num)
end_time = time.time() - start_time
print("time : {}".format(end_time), "[sec]")

NumbaやJITについてわかりやすかった参考記事:
qiita.com


NumbaやJITの効果を実感するため、以下のテストを行った。

テスト内容

①通常(CPython)のfor文
②通常(CPython)の内包表記
③①に@njit(No Python mode)
④①に@jit(paralell=True)(並列処理)
⑤②に@njit(No Python mode)
⑥②に@jit(paralell=True)(並列処理)

①~⑥のroop_numを10万回、100万回、1000万回に設定して処理時間を計測。

テスト結果

10万回:
①0.008977413177490234 [sec]
②0.006005525588989258 [sec]
③0.06488180160522461 [sec]
④0.07277131080627441 [sec]
⑤0.06978225708007812 [sec]
⑥0.07380175590515137 [sec]

100万回:
①0.09178948402404785 [sec]
②0.06682085990905762 [sec]
③0.08785486221313477 [sec]
④0.09371423721313477 [sec]
⑤0.09380340576171875 [sec]
⑥0.09977030754089355 [sec]

1000万回:
①0.8596999645233154 [sec]
②0.6133923530578613 [sec]
③0.36407947540283203 [sec]
④0.3690481185913086 [sec]
⑤0.3919491767883301 [sec]
⑥0.3789706230163574 [sec]

上記結果からわかる特徴・まとめ

・10万回まではJITは使わない方が速く、100万回で同等レベルの速さ、1000万回でJITの方が速くなる。
 →ある程度大きなデータやループ回数の多い関数で扱わないと効果を期待できない。
 →JITには並列処理もあるが、これも1000万回を超えないと効果が期待できないため、使い分けが必要。

・一般的にfor文よりも内包表記が高速と考えられているが、JIT実行時はfor文の方が、ループ回数問わず速い。
 →JITでループ関数を作るときはfor文を使用すべき!

おまけ

ここにはソースコードを載せていないが、もう少し複雑な関数でJITを試したところ、通常時よりも遅くなることもあった
これは、関数内部の変数の型が推定できず、PythonオブジェクトからCAPIを呼ぶというオブジェクトモードに切り替わっていたためと推測。
JITを使った高速化は手軽に実装できるが、JITをデコレーターとしてつける関数は、極力シンプルにしないと高速化できない