遅いPythonを高速化するためにNumbaをインストール

遅いPythonを高速化するためにNumbaをインストール プログラミング

Pythonを高速化したい人は、是非ともご覧ください。
冗談抜きで、本当に高速になります。

Pythonコードのコンパイルと聞くと、大変そうに思うかもしれません。
でも、 Numbaを利用すれば、全然大変ではありません。

Numbaのインストールも利用も、普通に簡単です。

本記事の内容

  • Numbaとは?
  • Numbaのシステム要件
  • Numbaのインストール
  • Numbaの動作確認

それでは、上記に沿って解説していきます。

Numbaとは?

Numba公式
https://numba.pydata.org/

Numbaは、Pythonのコードを高速なマシンコードに変換するJITコンパイラとなります。
正確には、NumbaはPythonのライブラリですけどね。

高速化する対象は、演算処理部分(Numpy)となります。
Numbaを利用すれば、Pythonの数値アルゴリズムをCやFORTRANの速度に近づけることが可能とのこと。
それであるのに、Numbaの利用方法はとても簡単と言えます。

@jit('void(double[:], double[:], double[:])', nopython=True, nogil=True)

上記のような呪文をコードに記述するだけです。
これだけで数十倍も処理が高速になったら、嬉しくないですか?

これは、嘘のような本当の話なのです。
試してみるのは簡単なので、是非とも試してみてください。

詳細は後述しますが、ソフトウェアを別途インストールする必要はありません。
パスを通したり、設定ファイルを変更することもありません。
あくまで、ライブラリをインストールするだけです。

以上、 Numbaについて説明しました。
次は、Numbaのシステム要件を確認しましょう。

Numbaのシステム要件

現時点(2021年7月)でのNumbaの最新バージョンは、0.53.1となります。
この最新バージョンは、2021年3月26日にリリースされています。

サポートOSに関しては、以下を含むクロスプラットフォーム対応です。

  • Windows
  • macOS
  • Linux

基本的には、IntelのCPUで動くことが想定されています。
Linuxの場合は、ARMでも動くようです。

そして、サポート対象となるPythonのバージョンは以下。

  • Python 3.6
  • Python 3.7
  • Python 3.8
  • Python 3.9

これは、下記のPythonの公式開発サイクル通りです。

バージョンリリース日サポート期限
3.62016年12月23日2021年12月
3.72018年6月27日2023年6月
3.82019年10月14日2024年10月
3.92020年10月5日2025年10月

やはり、Anacondaがスポンサーであるだけはありますね。
このようなところは、ちゃんとしています。

あとは、次の二つが公式には明示されています。

  • llvmlite 0.36.*
  • NumPy >=1.15

すでにインストール済みなら、バージョンには注意が必要です。
まだ、未インストールであれば、無視してOK。

自動的にpipで必要なモノをインストールしてくれます。

以上、Numbaのシステム要件についての説明でした。
次は、Numbaをインストールしていきます。

Numbaのインストール

最初に、Pythonのバージョンを確認しておきます。

>python -V
Python 3.9.6

次に、現状のインストール済みパッケージを確認します。

>pip list
Package    Version
---------- -------
pip        21.1.3
setuptools 57.2.0

次にするべきことは、pipとsetuptoolsの更新です。
pipコマンドを使う場合、常に以下のコマンドを実行しておきましょう。

python -m pip install --upgrade pip setuptools

では、Numbaのインストールです。
Numbaのインストールは、以下のコマンドとなります。

pip install numba

インストールは、少しだけ時間がかかります。
では、どんなパッケージがインストールされたのかを確認しましょう。

>pip list
Package    Version
---------- -------
llvmlite   0.36.0
numba      0.53.1
numpy      1.21.0
pip        21.1.3
setuptools 57.2.0

依存するnumpyとllvmliteのパッケージが、インストールされています。
そして、「llvmlite.dll」が自動的にダウンロード済みです。
※Linuxの場合は「libllvmlite.so」となる

したがって、LLVM(ソフトウェア)をわざわざインストールする必要がありません。
環境が汚れないので、素晴らしいです。

以上、Numbaのインストールについて説明しました。
次は、Numbaの動作確認を行います。

Numbaの動作確認

公式から、以下のサンプルコードを持ってきました。
このコードを使うと、Numbaの動作確認が可能です。

さらには、Numbaの威力も体感できます。
実際に試す場合には、CPUのコア数に注意してください。

import math
import threading
from timeit import repeat

import numpy as np
from numba import jit

nthreads = 4
size = 10**6

def func_np(a, b):
    """
    Control function using Numpy.
    """
    return np.exp(2.1 * a + 3.2 * b)

@jit('void(double[:], double[:], double[:])', nopython=True, nogil=True)

def inner_func_nb(result, a, b):
    """
    Function under test.
    """
    for i in range(len(result)):
        result[i] = math.exp(2.1 * a[i] + 3.2 * b[i])

def timefunc(correct, s, func, *args, **kwargs):
    """
    Benchmark *func* and print out its runtime.
    """
    print(s.ljust(20), end=" ")
    # Make sure the function is compiled before the benchmark is
    # started
    res = func(*args, **kwargs)
    if correct is not None:
        assert np.allclose(res, correct), (res, correct)
    # time it
    print('{:>5.0f} ms'.format(min(repeat(
        lambda: func(*args, **kwargs), number=5, repeat=2)) * 1000))
    return res

def make_singlethread(inner_func):
    """
    Run the given function inside a single thread.
    """
    def func(*args):
        length = len(args[0])
        result = np.empty(length, dtype=np.float64)
        inner_func(result, *args)
        return result
    return func

def make_multithread(inner_func, numthreads):
    """
    Run the given function inside *numthreads* threads, splitting
    its arguments into equal-sized chunks.
    """
    def func_mt(*args):
        length = len(args[0])
        result = np.empty(length, dtype=np.float64)
        args = (result,) + args
        chunklen = (length + numthreads - 1) // numthreads
        # Create argument tuples for each input chunk
        chunks = [[arg[i * chunklen:(i + 1) * chunklen] for arg in
                   args] for i in range(numthreads)]
        # Spawn one thread per chunk
        threads = [threading.Thread(target=inner_func, args=chunk)
                   for chunk in chunks]
        for thread in threads:
            thread.start()
        for thread in threads:
            thread.join()
        return result
    return func_mt

func_nb = make_singlethread(inner_func_nb)
func_nb_mt = make_multithread(inner_func_nb, nthreads)

a = np.random.rand(size)
b = np.random.rand(size)

correct = timefunc(None, "numpy (1 thread)", func_np, a, b)
timefunc(correct, "numba (1 thread)", func_nb, a, b)
timefunc(correct, "numba (%d threads)" % nthreads, func_nb_mt, a, b)

上記のままだと、Numbaが機能します。
以下のコードを無効(コメントアウト)にすると、Numbaは機能しません。

@jit('void(double[:], double[:], double[:])', nopython=True, nogil=True)

では、それぞれの結果を確認しましょう。
なお、利用マシンのCPUはインテル Core i9 9900KFです。

NumbaありNumbaなし
numpy (1 thread)        55 ms
numba (1 thread)        31 ms
numba (4 threads)       13 ms
numpy (1 thread)        59 ms
numba (1 thread)      1932 ms
numba (4 threads)     1956 ms

興味深い結果が、出ています。

  • numba (1 thread)の結果
  • numba (4 threads)の結果

それぞれを見ていきます。

numba (1 thread)の結果

ここの値を比較すれば、1コアにおけるNumbaの威力を確認できます。
単純に、62倍の処理速度の差があると言えます。

1932 ÷ 31 = 62.32・・・

大々的には、この数字を言うことが適切なのでしょう。

numba (4 threads)の結果

ここの値を比較すれば、4コアにおけるNumbaの威力を確認できます。
並列処理の場合には、Numbaはさらにその効果を発揮すると言えます。

1956 ÷ 13 = 150.46・・・

処理速度の差は、150倍です。

まとめ

Numbaの偉大さが、十分にわかる結果となりました。
62倍~150倍の性能差を確認できました。

この結果を見ると、Numbaを利用しないという選択肢はありませんね。

タイトルとURLをコピーしました