Edited at

Pythonの性能を学べばいいことが待っている、きっと……

タイトルはこれで適当につけています。


きっかけ

最近何かとPythonの性能について悩まされることが多かったので、NumbaやCythonの使い勝手も含めてちょっと調べてみたかった。


参考

https://numba.pydata.org/numba-doc/latest/user/5minguide.html

https://myenigma.hatenablog.com/entry/2017/03/02/155433

http://www.randpy.tokyo/entry/numba_cython


素の状態で単純にループを計測

とりあえずDataFrameに何かを読みだして、それとは別のリストに数字をランダムで出力。

数字リストの数字と一致するインデックス番号のデータを順次出力みたいな処理

読みだすものを作るのがめんどくさかったので先日作った平成300の顔のリストを流用。

import pandas as pd

import random
import timeit
import numpy
df = pd.read_csv(r"C:\PJ\02_app\20190430_H300Face\list.txt", encoding='shift_jis')
list_count = 1000
list_num = [random.randint(0, 299) for i in range(list_count)]


DataFrameの件数:300レコード

数字リストの件数:1000件



1.ListのFor × DataFrameのiterrows()のFor、ログ出力付き

def loop_iterrows(list_num, df):

for index_list_num in list_num:
for index, row_key in df.iterrows():
if(index == index_list_num):
print("'{}', '{}',".format(index_list_num, row_key["name"]))

平均:16.525(s/回)

わかっていたけど、アホみたいに時間がかかる。


2.1のログ無し

def loop_iterrows_nolog(list_num, df):

for index_list_num in list_num:
for index, row_key in df.iterrows():
if(index == index_list_num):
# print("'{}', '{}',".format(index_list_num, row_key["name"]))
pass

平均:14.648(s/回)

速くはなっているが、そこまでではない感じ。


numba


インストール

pip install numba

もしくは

conda install numba


3.単純に@jitを追加

from numba import jit

@jit
def loop_iterrows_nolog_jit(list_num, df):
for index_list_num in list_num:
for index, row_key in df.iterrows():
if(index == index_list_num):
# print("'{}', '{}',".format(index_list_num, row_key["name"]))
pass

平均 15.160(s/回)

あんま速くない

調べてみると、そもそもnumbaではpandasのDataFrameは使えない?

→Numpy.arrayにする必要がある模様

https://numba.pydata.org/numba-doc/latest/user/5minguide.html


Numba is a just-in-time compiler for Python that works best on code that uses NumPy arrays and functions, and loops. The most common way to use Numba is through its collection of decorators that can be applied to your functions to instruct Numba to compile them. When a call is made to a Numba decorated function it is compiled to machine code “just-in-time” for execution and all or part of your code can subsequently run at native machine code speed!


というか、Numpy用だからNumbaなのかな?


Numpy.array化

ということで、先にiterrowsでのループをNumpy.arrayにしてみる

num_array = df["name"].values

def loop_numpy_array_nolog(list_num, num_array):
for index_list_num in list_num:
for index, row_key in enumerate(num_array):
# for index, row_key in df.iterrows():
if(index == index_list_num):
# print("'{}', '{}',".format(index_list_num, row_key["name"]))
pass

平均 0.0186(s/回)

一瞬で終わりでウケる。これ、Jit化しなくてもいいんじゃね?

一瞬すぎて性能比較がよくわからなくなると思ったので件数を増やしてみる。


DataFrameの件数:300レコード

数字リストの件数:1,000件 → 1,000,000件(一気に1,000倍)


平均 19.623(s/回)


改めてjit化

今度はこれをjit化させてみる

平均 45.60707(s/回)

くそ遅くなっている。なぜだ。。。


遅いので条件をまた変更


DataFrameの件数:300レコード

数字リストの件数:1000件



if文をなくしてみる。

def loop_numpy_array_nolog_noif(list_num, num_array):

for index_list_num in list_num:
for row_key in num_array:
pass

2のやつ
平均 0.0197 (s/回)

2のやつのJIT化
平均 0.0639 (s/回)

ログ無し
平均 0.0059 (s/回)

ログ無しのJIT化
平均 0.0312 (s/回)

やっぱりjit化しない方が速い。

なぜだ。。。


そもそも最適化できていなかった。

nopython=Trueをつけたらわかったけど、どうも文字列は最適化できていないっぽい。

https://numba.pydata.org/numba-doc/dev/user/jit.html


1.4.3. Signature specifications

Explicit @jit signatures can use a number of types. Here are some common ones:

void is the return type of functions returning nothing (which actually return None when called from Python)

intp and uintp are pointer-sized integers (signed and unsigned, respectively)

intc and uintc are equivalent to C int and unsigned int integer types

int8, uint8, int16, uint16, int32, uint32, int64, uint64 are fixed-width integers of the corresponding bit width (signed and unsigned)

float32 and float64 are single- and double-precision floating-point numbers, respectively

complex64 and complex128 are single- and double-precision complex numbers, respectively

array types can be specified by indexing any numeric type, e.g. float32[:] for a one-dimensional single-precision array or int8[:,:] for a two-dimensional array of 8-bit integers.


だとしても、なんでこんなにも「遅くなる」んだろう?

あと、グローバル変数のものを対象の関数内で使用することもできない感じ。

これはまあコンパイルする都合を考えたらそらそうかという感じだわ。

とすると、文字列込みのリストとかを含めてぶん回したいときはどう使うといいんだろう?


Cython はあきらめた

ついでにCythonについても改めてちゃんと調べてみたけど、

コードの準備がめんどくさそうなわりに、そこまで性能に差はでなさそう。

ついでに付け加えるとNumbaだと並列やGPU処理もできそうなので、こっちをさらに追った方よさそうな感じ。


サンプル

役に立つかどうかはわからないけど、今回作ったあれこれ。

# -*- coding: utf-8 -*-

# ---------------------------------------------------------------------------

import pandas as pd
import random
import timeit
import numpy
df = pd.read_csv(r"C:\PJ\02_app\20190430_H300Face\list.txt", encoding='shift_jis')

def loop_iterrows(list_num, df):
for index_list_num in list_num:
for index, row_key in df.iterrows():
if(index == index_list_num):
print("'{}', '{}',".format(index_list_num, row_key["name"]))

def loop_iterrows_nolog(list_num, df):
for index_list_num in list_num:
for index, row_key in df.iterrows():
if(index == index_list_num):
# print("'{}', '{}',".format(index_list_num, row_key["name"]))
pass

def loop_numpy_array_nolog(list_num, num_array):
for index_list_num in list_num:
for index, row_key in enumerate(num_array):
# for index, row_key in df.iterrows():
if(index == index_list_num):
# print("'{}', '{}',".format(index_list_num, row_key["name"]))
pass

def loop_numpy_array_nolog_noif(list_num, num_array):
for index_list_num in list_num:
for row_key in num_array:
pass

from numba import jit, f8, i1, i8, u1, u4, autojit
@jit
def loop_iterrows_nolog_jit(list_num, df):
for index_list_num in list_num:
for index, row_key in df.iterrows():
if(index == index_list_num):
# print("'{}', '{}',".format(index_list_num, row_key["name"]))
pass

@jit
def loop_numpy_array_nolog_jit(list_num, num_array):
for index_list_num in list_num:
for index, row_key in enumerate(num_array):
# for index, row_key in df.iterrows():
if(index == index_list_num):
# print("'{}', '{}',".format(index_list_num, row_key["name"]))
pass

# @jit(i8[:], u1[:])
# @jit('void(u4[:], i1[:])', nopython=True)
@jit(parallel=True)
def loop_numpy_array_nolog_noif_jit(list_num, num_array):
for index_list_num in list_num:
for row_key in num_array:
pass

g_num_array = df["name"].values
@jit('void(u4[:])', nopython=True)
def loop_numpy_array_nolog_noif_jit_2(list_num):
for index_list_num in list_num:
for row_key in g_num_array:
pass

# list_count = 1000000
list_count = 1000
list_num = [random.randint(0, 299) for i in range(list_count)]

# n = 10000
loop = 10
result = timeit.timeit('loop_iterrows(list_num, df)', globals=globals(), number=loop)
print(result / loop)

result = timeit.timeit('loop_iterrows_nolog(list_num, df)', globals=globals(), number=loop)
print(result / loop)

result = timeit.timeit('loop_iterrows_nolog_jit(list_num, df)', globals=globals(), number=loop)
print(result / loop)

num_array = df["name"].values
list_num_array = numpy.array(list_num)

result = timeit.timeit('loop_numpy_array_nolog(list_num, num_array)', globals=globals(), number=loop)
print(result / loop)

result = timeit.timeit('loop_numpy_array_nolog_jit(list_num, num_array)', globals=globals(), number=loop)
print(result / loop)

result = timeit.timeit('loop_numpy_array_nolog_noif(list_num, num_array)', globals=globals(), number=loop)
print(result / loop)

result = timeit.timeit('loop_numpy_array_nolog_noif_jit(list_num, num_array)', globals=globals(), number=loop)
print(result / loop)

result = timeit.timeit('loop_numpy_array_nolog_noif_jit_2(list_num_array)', globals=globals(), number=loop)
print(result / loop)

試行錯誤した後が残っているので、このまま全部使えるかと言ったら若干怪しい。。

パラレルやGPUモードはそのうち試してみたい。

とりあえず今回一番理解したことはどうしてもループさせたかったら、DataFrameではなくNumpy.arrayにしようということだった。。