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

タイトルは[これ](http://www.copymecha.com/index.php)で適当につけています。

# きっかけ
最近何かと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の顔のリスト](https://qiita.com/ryo_naka/items/f3301d8d890c278dff6b)を流用。

```python
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件


## １．ListのFor × DataFrameのiterrows()のFor、ログ出力付き

```python
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/回)
わかっていたけど、アホみたいに時間がかかる。

## ２．１のログ無し

```python
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
```

## ３．単純に@jitを追加
```python
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にしてみる

```python
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文をなくしてみる。
```python
def loop_numpy_array_nolog_noif(list_num, num_array):
    for index_list_num in list_num:
        for row_key in num_array:
            pass
```

|||
|:-----------------|------------------:|
|２のやつ|平均 0.0197 (s/回) |
|２のやつの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処理もできそうなので、こっちをさらに追った方よさそうな感じ。


# サンプル

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

```python
# -*- 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にしようということだった。。