Numbaでの配列の定義時間

# Numbaでの配列の定義時間
他記事でNumbaでの配列の定義時間が気になったので、測定しました。


### 測定
<details><summary>確認用コード（折りたたみ）</summary><div>

```python
import matplotlib.pyplot as plt
from numba import njit
import numpy as np
import timeit

@njit
def make_arr_float64(loop,arr_len):
    for _ in range(loop):
        #s = np.empty(arr_len,dtype="float64")
        s = np.ones(arr_len,dtype="float64")
    return s
@njit
def make_arr_int8(loop,arr_len):
    for _ in range(loop):
        #s = np.empty(arr_len,dtype="int8")
        s = np.ones(arr_len,dtype="float64")
    return s
@njit
def make_arr_int64(loop,arr_len):
    for _ in range(loop):
        #s = np.empty(arr_len,dtype="int64")
        s = np.ones(arr_len,dtype="int64")
    return s
@njit
def make_arr_bool(loop,arr_len):
    for _ in range(loop):
        #s = np.empty(arr_len,dtype="bool")
        s = np.ones(arr_len,dtype="bool")
    return s
@njit
def make_arr_float64_assignment(loop,arr_len):
    for _ in range(loop):
        s = np.ones(arr_len,dtype="float64")
        for i in range(arr_len):
            s[i] = 0
    return s
@njit
def make_arr_int8_assignment(loop,arr_len):
    for _ in range(loop):
        #s = np.empty(arr_len,dtype="int8")
        s = np.ones(arr_len,dtype="float64")
        for i in range(arr_len):
            s[i] = 0
    return s
@njit
def make_arr_int64_assignment(loop,arr_len):
    for _ in range(loop):
        #s = np.empty(arr_len,dtype="int64")
        s = np.ones(arr_len,dtype="int64")
        for i in range(arr_len):
            s[i] = 0
    return s
@njit
def make_arr_bool_assignment(loop,arr_len):
    for _ in range(loop):
        #s = np.empty(arr_len,dtype="bool")
        s = np.ones(arr_len,dtype="bool")
        for i in range(arr_len):
            s[i] = False
    return s

def timer_for_make_arr(loop, arr_len,func):
    func(1,arr_len)
    t = timeit.timeit(lambda: func(loop,arr_len), number=1)/loop
    return t

def ploter_for_make_arr():
    loop = 100
    x = 2**np.arange(28)
    plt.xscale('log')
    plt.yscale('log')
    plt.xlabel("arr_len")
    plt.ylabel("time (s)")
    y = np.array([timer_for_make_arr(loop, arr_len,make_arr_float64) for arr_len in x])
    plt.plot(x,y,label="make_arr_float64")
    print("make_arr_float64 : Done")
    y = np.array([timer_for_make_arr(loop, arr_len,make_arr_int64) for arr_len in x])
    plt.plot(x,y,label="make_arr_int64")
    print("make_arr_int64 : Done")
    y = np.array([timer_for_make_arr(loop, arr_len,make_arr_int8) for arr_len in x])
    plt.plot(x,y,label="make_arr_int8")
    print("make_arr_int8 : Done")
    y = np.array([timer_for_make_arr(loop, arr_len,make_arr_bool) for arr_len in x])
    plt.plot(x,y,label="make_arr_bool")
    print("make_arr_bool : Done")
    y = np.array([timer_for_make_arr(loop, arr_len,make_arr_float64_assignment) for arr_len in x])
    plt.plot(x,y,label="make_arr_float64_assignment")
    print("make_arr_float64_assignment : Done")
    y = np.array([timer_for_make_arr(loop, arr_len,make_arr_int64_assignment) for arr_len in x])
    plt.plot(x,y,label="make_arr_int64_assignment")
    print("make_arr_int64_assignment : Done")
    y = np.array([timer_for_make_arr(loop, arr_len,make_arr_int8_assignment) for arr_len in x])
    plt.plot(x,y,label="make_arr_int8_assignment")
    print("make_arr_int8_assignment : Done")
    y = np.array([timer_for_make_arr(loop, arr_len,make_arr_bool_assignment) for arr_len in x])
    plt.plot(x,y,label="make_arr_bool_assignment")
    print("make_arr_bool_assignment : Done")
    plt.legend()
    plt.savefig("qiita_numba_make_arr_ones.png", format="png", dpi=400)
    plt.show()
ploter_for_make_arr()

```
</div></details>

### 結果
#### `np.empty`を使用
loop = 100000で実行

<div align="center"><img width="500" alt="image" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1278266/dcca240c-54ec-fd96-06df-89b8270e52a9.png"></div>

float64,int64とint8,boolの2種に分かれていてちょうどarr_lenを8だけスライドすると重なる。
numpyはboolがなぜか1bitではなく1byteなので、np.emptyにかかる時間はデータサイズごとに決まっていそう。
int8のグラフはarr_lenがそのままbytesに変わるので、2**20 Byteを超えると途端に時間がかかっている
大体1メガバイト、うーんなんかあるんですかね。
ちなみに使っているSoc M1の高性能コアは、「192KBのL1命令キャッシュと128KBのL1データキャッシュ、12MBのL2キャッシュは高性能コア内で共有」だそう。関係あるかは不明

#### `np.ones`を使用
loop = 100で実行。`x = 2**np.arange(28)`までに

<div align="center"><img width="500" alt="image" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1278266/88e1f405-8c75-ca4d-8578-aef78d898ffd.png"></div>

今回は、onesを1回するだけのものと、onesを0埋めするもの(assignment)の二つをとった。
`np.ones`は最初に1埋めするため、埋める時間がどんなもんなのかということをやっている。
結果見ての通り埋める時間はたいしたことない。
boolのみこれまた8ほどズレている。今回int8はズレていない。np.onesではbool有利か。

**1kBくらいまでの配列は長さ最小の配列と同じくらいの時間を食う**

<div align="center"><img width="500" alt="image" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1278266/3db997de-bc83-ac02-00da-95aa9d8757d7.png"></div>


次に、emptyを1埋めするもの(assignment)との比較。
今度は(見にくいが)int8のempty1埋めがboolと同じになっている。
あんまりint8使う人いないだろうから最適化されてないのかな。

### 考察
int8のグラフはarr_lenがそのままbytesに変わるのでその感じで読む。
ただ配列を定義するだけなら、emptyの方がonesより大幅にはやい。
配列の定義にかかる時間に比べて代入にかかる時間はたいしたことない。
が、emptyの1埋めはonesと同じくらい。
ので、emptyはほぼなんもしていないのでは？と思う。

配列の定義時間を測りたいときにはemptyではダメなのかなと思う。

そして、**1kBくらいまでの配列は長さ最小の配列と同じくらいの時間を食う**
これを以下のどちらで受け止めればいいかはまだよくわからない。
- 小さい配列を複数定義する場合はなるべくまとめろ
- 小さい配列を作る時間は1kBくらいまでたいしたことないからシビアにならなくていい

どっちかは処理にもよるだろうと思う。