Numbaを使ってPythonを100倍高速化する方法

スポンサーリンク

NumbaはPythonの配列関数と数値関数のコンパイラで、Pythonで直接書かれた高性能な関数でアプリケーションを高速化する力を与えてくれます。

スポンサーリンク

Python が遅いのはなぜ?

Pythonは長い間、科学計算のために使われてきました。

Pythonはプロトタイピングには最適な言語ですが、素のpythonはこのような巨大な計算を行うための最先端に欠けています。

Pythonを本質的に遅くしているのは、皮肉にもPythonが言語として非常に人気がある特徴なのです。

ひとつひとつ確認していきましょう。

  • 動的型付け。Pythonは動的型付け言語です。つまり、ユーザーは変数に関連するデータ型を指定する必要がありません。このため、表面上は非常にシンプルですが、インタプリタが操作のたびにデータ型と関連する変換をチェックする必要があるため、内部機構は何倍も複雑になります。このような命令の増加と複雑化が、Pythonのスピードの主な原因です。
  • メモリのオーバーヘッド Pythonの柔軟な性質のため、リスト内のintのような小さなオブジェクトごとに個別のメモリを確保する必要があります(配列のためにメモリの連続した塊を取るC言語とは異なります)。これは、リスト内のオブジェクトがメモリ上で互いに近くに配置されないことを意味し、各フェッチ操作の時間コストに影響します。
pip install numba
  • 非コンパイル。LLVMやGCCのようなコンパイラは、プログラムを先読みして高レベルの最適化を行うことができ、メモリと速度の両方を節約することができます。一方、Pythonインタープリタは、次の実行行を知らないので、時間を節約する最適化を適用することができません。
  • GILロック GILロック:Global Interpreter Lock(GIL)はマルチスレッドを許しません。GILロック:Global Interpreter Lock(GIL)はマルチスレッドを許しません。これは、オブジェクトモデルを同時アクセスに対して暗黙的に安全にすることで、CPythonの実装を簡素化します。

この記事では、numbaがどのようにこれらの困難を克服し、C/C++やFORTRANのようなコードを高速化するためにどのように使用できるかを見ていきます。

from numba import jit
import numpy as np
 
@jit            # Placing the @jit marks the function for jit compilation
def sum(a, b):
    return a + b

Numbaとは?

公式ドキュメントによると、「NumbaはPython用のジャストインタイムコンパイラで、NumPyの配列や関数、ループを使うコードに最適です」とあります。

JITコンパイラは、インタプリタ型言語の性能を向上させる上で実績のある手法の一つです。

プログラムの実行中、LLVMコンパイラはコードをネイティブコードにコンパイルします。

これは通常、インタプリタ版のコードよりもずっと高速です。

先に説明したように、コンパイラは高レベルの最適化を追加することができ、これはメモリと速度の両方の点でユーザーに利益をもたらします。

NumbaはAnacondaディストリビューションに付属しており、またホイールでも提供されています。

conda install numba

または

@jit(int32(int32, int32))
def sum(a, b):
    return a + b

注意:Linuxユーザーは、pipの代わりにpip3を使用する必要があるかもしれません。

PythonでNumbaを使う

Numbaは、関数の速度を向上させるために関数デコレータを使用します。

ユーザーが計算を関数内部で囲むことが重要です。

numbaで最も広く使われているデコレータは、@jitデコレータです。

このデコレーターを使うと、NumbaのJITコンパイラーによる最適化のために関数をマークすることができます

それでは、簡単な関数の使用例を見てみましょう。

@jit(nogil=True)
def sum(a, b):
    return a + b

Numbaは最初の実行までコンパイルを保持します。

最初の実行の間、numbaは入力型を推測し、その情報に基づいてコードをコンパイルします。

また、コンパイラは、その入力データ型に特化した最適化を追加します。

この直接的な結果として、関数は変数の種類によって異なる実行コードを持つことになります。

ユーザが初めて関数を実行するとき、多少の遅れを感じることがあります。

これは関数のコンパイルに起因するものです。

コンパイルが終わると、ユーザは通常のヌンバコンパイルされた関数の速度を期待できるようになります。

よくあるトリックとしては、初回実行時に小さなダミー変数を使用することです。

注意:関数内部で変数のデータ型を変更しないでください。

データ型を変更すると、numbaがデータ型を推測して関数を適切に最適化することができなくなります。

1. イージーモード

この方法の欠点は、最初の実行までコンパイルを待たなくてはならないことです。

この欠点は、イーガーモードによって克服することができます

イーガーモードでは、入力のデータ型を指定することで、コンパイラは入力から推論する必要がなく、関数を一度にコンパイルすることができます

これをイーガー実行と呼びますが、ここではその方法を説明します。

@jit(nopython=True)
def sum(a, b):
    return a + b

コンパイラはもう最初の実行を待つ必要はなく、与えられた型に対する特殊化を適用してコードをコンパイルします。

これにより、ユーザは使用する変数の種類をより多く制御できるようになります。

2. GILモードなし

コードをコンパイルすると、pythonのGlobal Interpreter Lockから解放されます。


nogil=True`でGILを使わないように指定することができます

from numba import jit
import numpy as np
 
x = np.arange(100).reshape(10, 10)
 
@jit(nopython=True)
def go_fast(a): # Function is compiled to machine code when called the first time
    trace = 0.0
    for i in range(a.shape[0]):   # Numba likes loops
        trace += np.tanh(a[i, i]) # Numba likes NumPy functions
    return a + trace              # Numba likes NumPy broadcasting
 
print(go_fast(x))

3. No-Pythonモード

実行モードには、nopythonモードとobjectモードがあります。

nopythonモードでは、コンパイラはインタプリタを介さずにコードを実行します。

numba.jit()を使ってコンパイルするのが最も良い方法です。

Array Vs List Numba
Python memory cost for list compared to numpy implementation of arrays.

Numbaはnumpyの配列と関数で最もうまく動作します。

以下は、numpyの関数を使った公式ドキュメントの例です。

Screenshot From 2021 03 17 18 31 46

まとめ

Numbaは、Pythonの構文上の特徴に影響を与えることなく、C/C++、FORTRAN、Javaなどに比べて高速に処理することが可能です。

numbaの欠点は、pythonのコードの柔軟性が低くなることですが、変数に対する細かい制御が可能になります。

Pythonを使用して、高速な処理と並列化機能を必要とする重い科学的シミュレーションを行う場合、Numbaはあなたの生活を容易にします。

参考文献

  • https://numba.pydata.org/numba-doc/latest/user/5minguide.html
  • https://numba.pydata.org/numba-doc/latest/user/jit.html
タイトルとURLをコピーしました