Pythonの高速化手法を比較してみました

Docker

Python高速化手法の処理時間比較

Python関連で様々な高速化方法が存在しますが、どれがどのくらい速いの?ということを確認するため、実際に環境構築から処理時間測定までを行いました。
結果は以下になります。

実行方法処理時間
[ms]
コメント評価方法
python3.6.93375・みんなが使うものリンク
python3.6.9
(docker)
4772・dockerはちょっと遅いリンク
python2.7.173056・3系に比べてなぜか速いリンク
Numba375・プログラムの微修正で高速化可能リンク
Cython151・速い!!
・プログラムにいろいろ追記が必要
 (ほとんどC言語なんじゃ・・・)
リンク
PyPy3728・プログラム修正無しで動いた
・インストールがめんどう
リンク
PyPy2741・こっちはapt-getでインストール可能リンク
Julia211・期待の新言語
・pythonっぽい書き方だが速い
・インストールがめんどう
リンク
C言語(-O2)151・Cythonと同等の速度リンク
C言語(-O3)150・Cythonと同等の速度リンク

処理時間測定には、ifや算術演算がループ処理の中に存在する素数判定アルゴリズムを用いました。2~10000までの素数の数をカウントするプログラムの処理時間を測定しています。
また、処理時間はすべて10回測定しその平均を記載しています。

測定環境
  • GCP(Google Cloud Platform)
  • Ubuntu18.04 LTS
  • マシンタイプ:N1シリーズ(n1-standard-1)
  • CPU:Intel(R) Xeon(R) CPU @ 2.00GHz 1コア
  • メモリサイズ:3.75GB
  • ディスク:SSD

評価方法

python

インストール

python2系と3系のインストール方法です。Ubuntu18.04の場合既にインストール済みのはずです。

sudo apt-get update
sudo apt-get install python
sudo apt-get install python3

評価用プログラム

素数カウント用サンプルコードです。
python3_base.pyというファイル名で保存します。
(2系も3系も同じコードで動きます)

import time
def prime_jud(num):
    ret = 1
    for i in range(2, num):
        if (num%i) == 0:
           ret = 0 
    return ret
def prime_count(max_num):
    cnt = 0
    for i in range(2, max_num):
        cnt += prime_jud(i)
    return cnt
if __name__ == "__main__":
    max_num = 10000
    start = time.time()
    ret = prime_count(max_num)
    end = time.time()
    print(ret)
    print('time:'+str(end-start)+str(' sec'))

実行方法

2系の場合

python python3_base.py

3系の場合

python3 python3_base.py

結果(2系,3系共通)

1229
time:3.359515905380249 sec

2~10000までの素数が1229個存在しその処理に3.36秒かかったことを表しています。

Docker

インストール

Dockerのインストール方法は別ページに記載しています。

pythonコマンドのDockerHubからのダウンロードとコンテナ化とバージョン確認を行います(欲張りすぎ・・・)。

sudo docker run -it --rm --name my-running-script -v "$PWD":/usr/src/myapp -w /usr/src/myapp python:3.6.9 python -V

下記が表示されると成功です。

Python 3.6.9

評価プログラム

python3と同じものを利用します。

実行方法

docker経由のpython実行コマンドです。

sudo docker run -it --rm --name my-running-script -v "$PWD":
/usr/src/myapp -w /usr/src/myapp python:3.6.9 python python3_base.py

下記の表示で成功です。

1229
time:4.709121465682983 sec

Numba

インストール

続いてNumbaになります。
pip3を入れていない人は下記で入ります。

sudo apt-get install python3-pip

Numba公式ページに従いインストールします。

pip3 install numba
pip3 install colorama==0.3.9

評価プログラム

python3_numba.pyというファイル名で保存します。
素のpython3プログラムとの違いはnumbdaをimportしているところと、関数先頭に、@numba.jitを記載している部分になります。

import numba
import time
@numba.jit
def prime_jud(num):
    ret = 1
    for i in range(2, num):
        if (num%i) == 0:
           ret = 0
    return ret
def prime_count(max_num):
    cnt = 0
    for i in range(2, max_num):
        cnt += prime_jud(i)
    return cnt
if __name__ == "__main__":
    max_num = 10000
    start = time.time()
    ret = prime_count(max_num)
    end = time.time()
    print(ret)
    print('time:'+str(end-start)+str(' sec'))

実行方法

python3と同じです。

python3 python3_numba.py

下記実行結果になります。明らかに速くなっています

1229
time:0.3827321529388428 sec

Cython

インストール

続いてCythonになります。
Cython公式ページに従いインストールします。

pip3 install cython

評価プログラム

Cythonプログラムは変数や関数戻り値の型を厳密の定義する必要があります。また拡張子も.pyから.pyxへ変更する必要があります。
このプログラムをpython3_cython.pyxとして保存します。

import cython
import time
cdef prime_jud(int num):
    cdef:
        int i, ret
    ret = 1
    for i in range(2, num):
        if (num%i) == 0:
           ret = 0 
    return ret
def prime_count(int max_num):
    cdef:
        int i, cnt
    cnt = 0
    for i in range(2, max_num):
        cnt += prime_jud(i)
    return cnt

pythonから呼び出す関数をdefとして定義し、Cプログラム内で利用する関数をcdefとして定義します。

python3_cython.pyxをコンパイルするためのプログラムを作成します。
ファイル名はsetup.pyとします。

from distutils.core import setup, Extension
from Cython.Build import cythonize
from numpy import get_include # cimport numpy を使うため
ext = Extension("python3_cython", sources=["python3_cython.pyx"], include_dirs=['.', get_include()])
setup(name="python3_cython", ext_modules=cythonize([ext]))

コンパイルした関数(prime_count)をpythonプログラムから呼び出す部分を作成します。
ファイル名はpython3_cython.pyとします。

import time
from python3_cython import prime_count
import python3_cython
if __name__ == "__main__":
    max_num = 10000
    start = time.time()
    ret = prime_count(max_num)
    end = time.time()
    print(ret)
    print('time:'+str(end-start)+str(' sec'))

実行方法

はじめにコンパイルを行います。

python3 setup.py build_ext --inplace

次に実行します。

python3 python3_cython.py

下記実行結果になります。ふー長かった。でも速い。

1229
time:0.15123748779296875 sec

PyPy

PyPy2系と3系でインストール方法が異なります。

インストール(PyPy2)

まずは2系です。

sudo apt-get update
sudo apt-get install pypy

バージョン確認

pypy -V
Python 2.7.13 (5.10.0+dfsg-3build2, Feb 06 2018, 18:37:50)
[PyPy 5.10.0 with GCC 7.3.0]

インストール(PyPy3)

PyPy公式Downloadページ⇒Python 3.6 compatible PyPy3.6 v7.3.1⇒Linux x86-64 binary (64bit, built on CentOS6)からビルド環境のパスを取得。


ここでは、wgetを用いて取得しますがブラウザ等で取得しても問題ありません。各自好きな場所に配置しtarで展開してください。

wget https://bitbucket.org/pypy/pypy/downloads/pypy3.6-v7.3.1-linux64.tar.bz2
tar xvf pypy3.6-v7.3.1-linux64.tar.bz2

ビルド済みの環境のためそのまま実行可能です。
そのため、実行フォルダに対してPATHを通します。

echo "export PATH=~/pypy3.6-v7.3.1-linux64/bin:$PATH" >> ~/.bashrc
source .bashrc

バージョン確認

pypy3 -V
Python 3.6.9 (2ad108f17bdb, Apr 07 2020, 02:59:05)
[PyPy 7.3.1 with GCC 7.3.1 20180303 (Red Hat 7.3.1-5)]

評価プログラム

python3と同じものを利用します。

実行方法

pypy2から

pypy python3_base.py 
1229
time:0.756889104843 sec

続いてpypy3です。

pypy3 python3_base.py 
1229
time:0.7219762802124023 sec

Julia

インストール

最後にJuliaです。
インストールにつきましてはこちらのHPを参考にさせて頂きました。
はじめに、gfortranをインストールしJuliaのビルド環境を取得します。

sudo apt-get update
sudo apt-get install gfortran
git clone https://github.com/JuliaLang/julia.git

次に、juliaフォルダへ移動しビルドを行います。
makeの実行には10分程度かかりますので気長に待ちましょう。
CPUコア数が1つのためか-j4オプションを付けるとビルドが停止してしまったので外しています。

cd julia
sh contrib/download_cmake.sh
make

最後にPATHを通して終わりです。

echo "export PATH=~/julia:$PATH" >> ~/.bashrc
source .bashrc

下記のバージョン確認ができればインストール完了です。

julia -v
julia version 1.4.1

評価プログラム

Julia言語を用いて同様のプログラムを(見よう見まねで)記載しました。
julia_base.jlというファイル名で保存します。

function prime_jud(num)
    ret = 1
    for i=2:num-1
        if (num%i) == 0
           ret = 0
        end
    end
    return ret
end
function prime_count(max_num)
    cnt = 0
    for i=2:max_num-1
        cnt += prime_jud(i)
    end
    return cnt
end
max_num = 10000
@time ret = prime_count(max_num)
println(ret)

実行方法

下記のように実行します。

julia julia_base.jl

下記実行結果になります。プログラムに自信がなかったのですが、素数の数が1229個と出力されていますので正しく計算できているようです。

  0.211689 seconds (20.80 k allocations: 1.254 MiB)
1229

C言語

gccのインストールや実行方法については、みなさんご存じと思いますので省略します。評価プログラムのみ記載します。

評価プログラム

#include <stdio.h>
#include <time.h>
int prime_jud(int num) {
    int ret = 1;
    for (int i=2; i<num; i++) {
        if ((num%i) == 0) {
           ret = 0; 
        }
    }
    return ret;
}
int prime_count(int max_num) {
    int cnt = 0;
    for (int i=2; i<max_num; i++) {
        cnt += prime_jud(i);
    }
    return cnt;
}
int main()
{
    int ret;
    int max_num = 10000;
    double start = clock();
    ret = prime_count(max_num);
    double end = clock();
    double time = (end - start) / CLOCKS_PER_SEC;
    printf("%d  %f\n", ret, time);
    return 0;
}

まとめ

Python関連の様々な高速化方法について処理時間を比較しました。やはりコンパイル系は速かったですね。中でも、Cythonは最も高速となりましたので、pythonプログラムをベースに高速化を実現したい場合には有力な選択肢になりそうです。

ただ、Cythonはプログラムの修正が必要であるため、プログラム修正することなくお手軽に高速化を試すにはPyPyが良さそうです。NumbaやPyPyはコード修正が軽微または無しで使えますが、ライブラリの対応や書き方等に制限があるためライブラリを多用する複雑なプログラムに対してどこまで移行できるかについては、よく確認する必要がありそうです。
まずは、お手軽に高速化を試すには、PyPyが最初の一歩になりそうです。

ここまで読んでいただきありがとうございました。

コメント

  1. test

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