読者です 読者をやめる 読者になる 読者になる

MyEnigma

とあるエンジニアのブログです。#Robotics #Programing #C++ #Python #MATLAB #Vim #Mathematics #Book #Movie #Traveling #Mac #iPhone

JITコンパイラライブラリNumbaを使ってPythonコードを劇的に高速化する方法

ふつうのコンパイラをつくろう 言語処理系をつくりながら学ぶコンパイルと実行環境の仕組み

ふつうのコンパイラをつくろう 言語処理系をつくりながら学ぶコンパイルと実行環境の仕組み

目次

はじめに

Pythonは言語的にも、

エコシステム的にも大好きなのですが、

いかんせん、複雑で大規模なシステムを作ると、

速度が遅いのが気になる時があります。

 

下記の記事のように、

Pythonの一部をC/C++コードに置き換えてもいいのですが、

やっぱりコードを置き換えるのは面倒ですね。。

(またコードを変換している時に、

バグを埋め込んでしまう可能性もあります)

myenigma.hatenablog.com

 

そこで、色々調べた所、

JIT (Just In Time) コンパイラという技術を利用した

Numbaというライブラリを使うことで、

ある特定のPythonコードを修正無しで、

かなり高速化できたので、

その方法を紹介したいと思います。

 

Python JITコンパイラライブラリ Numba

NumbaはJust In Time コンパイラ (実行時コンパイラ)

という技術を使って、既存のPythonコードを高速化するライブラリです。

Numba — Numba

github.com

 

このnumbaはAnacondaを開発している

Continuum Analytics, Incが開発しています。

 

通常のPythonコードは、スクリプト言語であるため、

毎行毎にコンパイルを実施しながら、

コードを実行します。

そのため、forループのようなコードを書くと、

毎回のコンパイルのオーバヘッドが大きく、

事前コンパイルでループ処理を最適化するC++などに比べて、

かなり処理時間が大きくかかってしまいます。

そのため、Pythonを高速化するためには、

ベクトル化と呼ばれる、行列演算などを駆使することで

高速化するのが一般的です。

(これはMATLABも同じです。

myenigma.hatenablog.com )

 

しかし、多少複雑な条件分岐などを使って、

行列を処理したい場合、

上記のベクトル化が難しい場合があります。

 

そんな時は、このNumbaを使うことで、

forループを使うコード(関数)を事前にコンパイルして、

最適化することで、

C/C++のようなパフォーマンスを得ることができるようです。

 

NumbaはLLVMという、

最新のコンパイラフレームワークを使って、

指定されたPython関数をコンパイルしてくれるため、

非常に高い性能を得ることができるようです。

 

Numbaのインストール

anacondaを使ったほうがいいようです。

自分の場合pyenvの環境に、

anacondaの環境を作って、

$ conda install numba

でインストールしました。

 

Numbaの速度比較

下記のような、

Pythonにおいては非常によくないコードがあるとします。

#! /usr/bin/python
# -*- coding: utf-8 -*-

from numpy import arange
import time

def sum2d(arr):
    M, N = arr.shape
    result = 0.0
    for i in range(M):
        for j in range(N):
            result += arr[i, j]
    return result


start = time.time()
a = arange(100000000).reshape(10000, 10000)
print(sum2d(a))
elapsed_time = time.time() - start
print ("elapsed_time:{0}".format(elapsed_time) + "[sec]")

内容としては10000x10000の行列の要素の和を

ループで足し合わせる関数です。

Pythonにとって、forループはご法度なのですが、

無理やり計算しています。

 

上記のコードを実行すると、

自分の環境の場合、

f:id:meison_amsl:20170302151231p:plain

計算時間に33秒ぐらいかかっています。

 

一方、下記のように、

numbaのjitモジュールをimportして、

先程のコードに@jitとデコレータを付けるだけで、

下記のsum2d関数がJITで最適化コンパイルされます。

#! /usr/bin/python
# -*- coding: utf-8 -*-

from numba import jit
from numpy import arange
import time

# jit decorator tells Numba to compile this function.
# The argument types will be inferred by Numba when function is called.


@jit
def sum2d(arr):
    M, N = arr.shape
    result = 0.0
    for i in range(M):
        for j in range(N):
            result += arr[i, j]
    return result


start = time.time()
a = arange(100000000).reshape(10000, 10000)
print(sum2d(a))
elapsed_time = time.time() - start
print ("elapsed_time:{0}".format(elapsed_time) + "[sec]")

上記のコードを実行すると、

同じ環境であっても、

f:id:meison_amsl:20170302151457p:plain

計算時間が0.7秒(!!!)になりました。

モジュールをインポートして、

一行コードを追加するだけで、

220倍も高速になりました。

元のコードを全く修正なくていいのが最高ですね。

 

引数の指定

下記の記事の通り、@jitだけでなく、

引数の型を指定することで、

より高速になるようです。

yutori-datascience.hatenablog.com

 

GPUの利用

Numbaは通常のCPU処理だけでなく、

GPUの処理にも対応しています。

 

ただ、下記の記事の通り、

先程は@jitのデコレータを追加するだけでしたが、

GPUを使う場合、GPUのライブラリであるCUDAをつかうために、

かなり処理を変更する必要があるようで、簡単には使えなそうです。

yutori-datascience.hatenablog.com

yutori-datascience.hatenablog.com

yutori-datascience.hatenablog.com

 

最後に

ほとんどPythonのコードに変更無しに

高速化できるのはすばらしいですね。

ライブラリではなく、

Pythonそのものに入れてもいいのではないかと思いました。

 

参考資料

dr-kayai.hatenablog.com

yutori-datascience.hatenablog.com

yutori-datascience.hatenablog.com

yutori-datascience.hatenablog.com

postd.cc

qiita.com

myenigma.hatenablog.com

myenigma.hatenablog.com

myenigma.hatenablog.com

myenigma.hatenablog.com

ふつうのコンパイラをつくろう 言語処理系をつくりながら学ぶコンパイルと実行環境の仕組み

ふつうのコンパイラをつくろう 言語処理系をつくりながら学ぶコンパイルと実行環境の仕組み