Cythonでnumpyを使った時の速度比較、高速化には何が必要か

#はじめに
Cythonとnumpyを使った時に、どうすれば高速化できるのか調べました。

##測定方法
大きさnのnumpyの配列を作り、その配列にn回代入と参照を行う関数func()と、
それをm回数実行するsum_func()を作りました。
今回は $n,m$ それぞれ $n=10000, m=100$ に設定しており、
sum_func()の実行時間を測定します。

これをjupyter notebookの「%timeit」を用いて測定しました。

## Python

```python
import numpy as np

def func1(n):
    A = np.empty(n)
    for i in range(n):
        A[i] = i
    
    sum = 0
    for i in range(n):
        sum += A[i]
        
    return sum / n

def sum_func1(n):
    res = 0
    m = 10000
    for i in range(n):
        res += func1(m)
       
    return res
```

###結果
```
%timeit sum_func1(100)
220 ms ± 3.31 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
```

結構遅いです。

##Cython(コンパイルだけ)

```python
%%cython

import numpy as np
cimport numpy as np
cimport cython

def func2(n):
    A = np.empty(n)
    for i in range(n):
        A[i] = i
    
    sum = 0
    for i in range(n):
        sum += A[i]
    
    return sum / float(n)

def sum_func2(n):
    res = 0
    m = 10000
    for i in range(n):
        res += func2(m)
        
    return res
```

###結果
```
%timeit sum_func2(100)
176 ms ± 3.93 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
```

1.25倍くらい速くなりました。

##Cython(型指定)

```python
%%cython

import numpy as np
cimport numpy as np
cimport cython

def func3(int n):
    cdef int i
    cdef double sum
    cdef np.ndarray A = np.empty(n)
    for i in range(n):
        A[i] = i
        
    sum = 0
    for i in range(n):
        sum += A[i]
        
    return sum / float(n)

def sum_func3(int n):
    cdef double res = 0
    cdef int m = 10000
    for i in range(n):
        res += func3(m)
        
    return res
```

###結果
```
%timeit sum_func3(100)
117 ms ± 2.55 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
```

ちょっとだけ速くなりました。

##Cython(numpyの詳細な型を指定)

```python
%%cython

import numpy as np
cimport numpy as np
cimport cython

ctypedef np.float64_t DTYPE_t

def func4(int n):
    cdef int i
    cdef double sum
    cdef np.ndarray[DTYPE_t, ndim=1] A
    A = np.empty(n)
    for i in range(n):
        A[i] = i
        
    sum = 0
    for i in range(n):
        sum += A[i]
        
    return sum / float(n)

def sum_func4(int n):
    cdef double res = 0
    cdef int m = 10000
    for i in range(n):
        res += func4(m)
        
    return res
```

###結果

```
%timeit sum_func4(100)
1.42 ms ± 4.45 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
```
numpyのデータタイプを指定すると、非常に速くなりました。
これが重要そうです。

##Cython(関数の返り値指定+cdef)

```python
%%cython

import numpy as np
cimport numpy as np
cimport cython

ctypedef np.float64_t DTYPE_t

cdef double func5(int n):
    cdef int i
    cdef double sum
    cdef np.ndarray[DTYPE_t, ndim=1] A
    A = np.empty(n)
    for i in range(n):
        A[i] = i
        
    sum = 0
    for i in range(n):
        sum += A[i]
        
    return sum

cpdef double sum_func5(int n):
    cdef double res = 0
    cdef int m = 10000
    for i in range(n):
        res += func5(m)
        
    return res
```

###結果

```
%timeit sum_func5(100)
1.34 ms ± 19.9 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
```

少しだけ速くなりました。

##Cython(関数のinline化)

```python
%%cython

import numpy as np
cimport numpy as np
cimport cython

ctypedef np.float64_t DTYPE_t

cdef inline double func6(int n):
    cdef int i
    cdef double sum
    cdef np.ndarray[DTYPE_t, ndim=1] A
    A = np.empty(n)
    for i in range(n):
        A[i] = i
        
    sum = 0
    for i in range(n):
        sum += A[i]
        
    return sum

cpdef inline double sum_func6(int n):
    cdef double res = 0
    cdef int m = 10000
    for i in range(n):
        res += func6(m)
        
    return res
```

###結果

```
%timeit sum_func6(100)
1.35 ms ± 30 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
```

ほとんど変わりません。

#比較

|        |       平均処理時間 |    改善倍率(倍)  |
|:-----------------|------------------:|:------------------:|
| Python         |              220 ms |        1.0       |
| Cython(コンパイルだけ)           |            176 ms |       1.25       |
| Cython(型指定)            |              117 ms |        1.88        |
| Cython(numpyの詳細な型を指定)              |                1.42 ms |         154         |
| Cython(関数の返り値指定+cdef)            |             1.34 ms|       164       |
| Cython(関数のinline化)          |           1.35 ms |      162       |

#まとめ

今回の測定の結果から、Cythonの高速に必要なことは

1.変数の型指定
2.numpyの詳細な型指定
3.関数の返り値を設定しcdefする

であるとわかりました。

#ソースコードについて
https://github.com/nena-undefined/til/blob/master/cython-numpy.ipynb