list、NumPy配列の1~3次元配列について、参照渡し、浅いコピー、深いコピーなどの複製時間を計測しました。速度の差を感覚的に知っておくことは重要だと思います。私自身、予想外の計測結果になったものもあり、楽しむことができました。
0. FEMなど数値解析シリーズ
この記事は「FEMなど数値解析シリーズ」の記事です。一連の記事は、以下のリンク集を参照してください。
1.計測方法
多くのケースの検証を行い、かなり長いコードになってしまいましたので、検証コードの一部をコード01に抜粋しました。このコードで検証方法の概要を説明したいと思います。
検証パターンは、NumPy配列がN1~N13の13パターン、listがL1~L10の10パターンです。コード01ではその中のN1, N3, N10, N11, L1, L3, L9, L10の8種類を抜き出しました。
いずれも、変数xに0~1のランダムな倍精度少数(list:float, NumPy:float64)を入力し、その値を複製します。変数名はxがNumPy配列の場合はx_npとし、xがlistの場合はx_listとします。
基本的に変数yに代入しますが、代入先にスライスを用いる場合は変数名をs_np[:]、s_list[:]とします。s_np[:]、s_list[:]は1回目の複製の前に要素をゼロで初期化しますが、2回目以降は初期化しません。ただし、初期化時間も計算時間に含んだN11、L10のみ、複製を行うdef関数を呼び出すたびにs_np, s_listを生成し直しゼロで初期化しました。
データの複製はdef関数の中で行い、いずれも複製を20000回行って計算時間を計測します。
コード01には1次元配列の部分しかありませんが、1次元配列の要素数は262144個、2次元配列の要素数は512×512 = 262144個、3次元配列の要素数は64×64×64 = 262144個であり、いずれも要素数は同じです。
#コード01
import numpy as np
import time
import random
def f(x):
y = x #N1, L1
def f_slice_left(x, s):
s[:] = x #N3, L3
def f_for1(x, s, dim):
for i in range(dim):
s[i] = x[i] #N10, N11, L9, L10
n_dim = 1 #検証する次元の指定
nnn = 20000
dim = 262144
x_np = np.random.rand(dim) #float64
x_list = [random.random() for i in range(dim)]
s_np = np.zeros(dim)
s_list = [0.0 for i in range(dim)]
#----------
print('===== NumPy配列のコピー =====')
print('y = x_np:') #N1
start = time.time() #時間計測開始
for i in range(nnn):
f(x_np)
print(time.time() - start)
print('s_np[:] = x_np:') #N3
start = time.time() #時間計測開始
for i in range(nnn):
f_slice_left(x_np, s_np)
print(time.time() - start)
print('for文_np, dim1:') #N10(1次元)
start = time.time() #時間計測開始
for i in range(nnn):
f_for1(x_np, s_np, dim)
print(time.time() - start)
print('for文_np, dim1:') #N11(1次元)
start = time.time() #時間計測開始
for i in range(nnn):
s_np = np.zeros(dim)
f_for1(x_np, s_np, dim)
print(time.time() - start)
print('===== listのコピー =====')
print('y = x_list:') #L1
start = time.time() #時間計測開始
for i in range(nnn):
f(x_list)
print(time.time() - start)
print('s_list[:] = x_list:') #L3
start = time.time() #時間計測開始
for i in range(nnn):
f_slice_left(x_list, s_list)
print(time.time() - start)
print('for文_list, dim1:') #L9(1次元)
start = time.time() #時間計測開始
for i in range(nnn):
f_for1(x_list, s_list, dim)
print(time.time() - start)
print('for文_list, dim1:') #L10(1次元)
start = time.time() #時間計測開始
for i in range(nnn):
s_list = [0.0 for i in range(dim)]
f_for1(x_list, s_list, dim)
print(time.time() - start)
2.NumPyの計測結果一覧
N4, N5は複製するだけでは無くlistからNumPy配列への型変換も行っています。
また、N6~N8は浅いコピー、N9は深いコピーを行うメソッドですが、NumPy配列ではデータの型がオブジェクト型となる特殊な場合を除き、いずれも複製前と複製後の全ての要素は互いに独立しており、片方の変数の変更は他方の変数に影響を与えません。つまり、listの深いコピーと同じようなコピーになります。
したがって、以下の計測結果のN6~N9は浅いコピー、深いコピーの違いや、複製する変数の次元の違いにかかわらず複製時間はほとんど変わりません。
また、N3は要素の値をコピーします。N3では「s_np[:] = x_np」の実行前にx_npと同じ大きさのs_npを定義しておく必要があります。そして、代入される側のs_npは代入前の参照先アドレスの状態がそのまま残ります。つまり、「s_np[:] = x_np」の実行前にs_npとx_npが互いに独立した変数であれば、独立した変数のままであり、参照渡しの状態であれば、参照渡しの状態のままとなります。listとは違い、N3のように値をコピーしても参照渡しの状態になることがあるので注意しましょう。
下表は複製時間(秒)の計測結果一覧です。
複製の方法 | 備考 | 1次元 | 2次元 | 3次元 | |
N1 | y = x_np | 参照渡し | 0.002 | 0.002 | 0.002 |
N2 | y = x_np[:] | 要素の参照渡し | 0.005 | 0.005 | 0.005 |
N3 | s_np[:] = x_np | 要素の値コピー | 1.750 | 2.562 | 2.158 |
N4 | s_np[:] = x_list | list→NumPy | 96.661 | 95.804 | 114.639 |
N5 | y = np.array(x_list) | list→NumPy | 114.534 | 111.620 | 127.565 |
N6 | y = np.copy(x_np) | (浅い)コピー | 18.278 | 18.001 | 18.339 |
N7 | y = x_np.copy() | (浅い)コピー | 18.175 | 18.270 | 18.020 |
N8 | y = copy.copy(x_np) | (浅い)コピー | 18.095 | 18.340 | 17.707 |
N9 | y = copy.deepcopy(x_np) | (深い)コピー | 18.211 | 18.165 | 17.576 |
N10 | 要素毎のコピー(for文) s[i][j][k] = x[i][j][k] | 要素のコピー | 707.420 | 2057.253 | 3186.018 |
N11 | N10のs[]を毎回初期化 | 要素のコピー | 725.598 | 2050.131 | 3236.215 |
N12 | 要素毎のコピー(for文) s[i, j, k] = x[i, j, k] | 要素のコピー | 707.420 (=N10) | 1078.563 | 1299.485 |
N13 | N12のs[]を毎回初期化 | 要素のコピー | 725.598 (=N11) | 1094.620 | 1308.806 |
下表は各次元のN1の計算時間を1としたときのN2~N11の計算時間の割合を示したものです。例えばN2の計算時間はN1の3倍であることがわかります。
複製の方法 | 備考 | 1次元 | 2次元 | 3次元 | |
N1 | y = x_np | 参照渡し | 1 | 1 | 1 |
N2 | y = x_np[:] | 要素の参照渡し | 3 | 3 | 3 |
N3 | s_np[:] = x_np | 要素の値コピー | 873 | 1278 | 1087 |
N4 | s_np[:] = x_list | list→NumPy | 48207 | 47775 | 57751 |
N5 | y = np.array(x_list) | list→NumPy | 57121 | 55661 | 64262 |
N6 | y = np.copy(x_np) | (浅い)コピー | 9116 | 8976 | 9239 |
N7 | y = x_np.copy() | (浅い)コピー | 9064 | 9111 | 9078 |
N8 | y = copy.copy(x_np) | (浅い)コピー | 9025 | 9145 | 8920 |
N9 | y = copy.deepcopy(x_np) | (深い)コピー | 9082 | 9058 | 8854 |
N10 | 要素毎のコピー(for文) s[i][j][k] = x[i][j][k] | 要素のコピー | 352810 | 1025888 | 1604988 |
N11 | N10のs[]を毎回初期化 | 要素のコピー | 361876 | 1022336 | 1630275 |
N12 | 要素毎のコピー(for文) s[i, j, k] = x[i, j, k] | 要素のコピー | 352810 (=N10) | 537846 | 654628 |
N13 | N12のs[]を毎回初期化 | 要素のコピー | 361876 (=N11) | 545853 | 659324 |
NumPyの複製時間の計測結果より、以下のようなことがわかります。
- コピー(N6~N9)の計算時間は参照渡し(N1)の約9000倍。オーダーとして1万倍ぐらい違います。できるだけ参照渡しを使いましょう。
- np.copy(x), x.copy(), copy.copy(x), copy.deepcopy(x)は、どれを使っても計算速度は大きく変わりません。
- N10, N11, N12, N13より、for 文を使って要素を1つずつコピーすると非常に遅いことがわかります。この傾向は次元が多くなるほど顕著です。NumPyでは要素を1つずつ扱うのではなく、できるだけまとめて扱うようにする必要があります。(後述のリストL9, L10よりも遅く、想像以上に遅いです)
- 要素を一つずつコピーする場合、s[i][j][k] = x[i][j][k] (N10, N11)よりも、s[i, j, k] = x[i, j, k](N12, N13)の方が速い。
- N3はN6~N9のコピーに比べて計算時間が10分の1程度です。N3のようにあらかじめ用意した配列に繰り返し代入できる場合は、N3の方法の方が効率的であることがわかります。
3.listの計測結果一覧
L4, L5は複製するだけではなくNumPy配列からlistへの型変換も行っています。
L2~L7は、次元が増えるにしたがって複製速度が速くなっていますが、これは1番浅い部分の要素数が違うこと(1次元:262144、2次元:512、3次元:64)が主な要因です。L2~L7は浅いコピーであるため一番浅い部分の要素数が計算速度に最も影響を与えます。
L4, L5の深い部分はNumPy配列のままであり、リストに型変換されるのは浅い部分だけです。
なお、NumPyのN2(y = x[:])は参照渡しですが、listのl2(y = x[:])は浅いコピーなので混同しないようにしましょう。
複製の方法 | 備考 | 1次元 | 2次元 | 3次元 | |
L1 | y = x_list | 参照渡し | 0.002 | 0.002 | 0.002 |
L2 | y = x_list[:] | 浅いコピー | 43.879 | 0.028 | 0.006 |
L3 | s_list[:] = x_list | 浅いコピー | 56.175 | 0.027 | 0.008 |
L4 | s_list[:] = x_np | NumPy→list | 268.926 | 1.263 | 0.176 |
L5 | y = list(x_np) | NumPy→list | 206.536 | 1.245 | 0.185 |
L6 | y = x_list.copy() | 浅いコピー | 44.392 | 0.029 | 0.007 |
L7 | y = copy.copy(x_list) | 浅いコピー | 45.307 | 0.039 | 0.013 |
L8 | y = copy.deepcopy(x_list) | 深いコピー | 2975.245 | 3001.622 | 3231.843 |
L9 | 要素毎のコピー(for文) | 要素のコピー | 292.382 | 387.693 | 519.536 |
L10 | N10の代入先を毎回初期化 | 要素のコピー | 523.983 | 554.042 | 725.232 |
下表は各次元のL1の計算時間を1としたときのL2~L10の計算時間の割合を示します。
複製の方法 | 備考 | 1次元 | 2次元 | 3次元 | |
L1 | y = x_list | 参照渡し | 1 | 1 | 1 |
L2 | y = x_list[:] | 浅いコピー | 21972 | 14 | 3 |
L3 | s_list[:] = x_list | 浅いコピー | 28130 | 13 | 4 |
L4 | s_list[:] = x_np | NumPy→list | 134666 | 630 | 87 |
L5 | y = list(x_np) | NumPy→list | 103424 | 621 | 91 |
L6 | y = x_list.copy() | 浅いコピー | 22230 | 15 | 3 |
L7 | y = copy.copy(x_list) | 浅いコピー | 22687 | 19 | 6 |
L8 | y = copy.deepcopy(x_list) | 深いコピー | 1489862 | 1496637 | 1591001 |
L9 | 要素毎のコピー(for文) | 要素のコピー | 146411 | 193307 | 255762 |
L10 | N10の代入先を毎回初期化 | 要素のコピー | 262386 | 276250 | 357024 |
主なポイントは以下の通りです。
- 浅いコピー(L6, L7)は参照渡し(L1)の2万倍以上遅いです。できるだけ参照渡しを使うようにしましょう。
- L8で使用したlistのcopy.deepcopy(x)は非常に遅いです。for文で要素を1つずつコピーした方が速く、想定以上の遅さでした。
- どんな場合でもNumPyがlistよりも高速なわけではなく、NumPyのN10, N11とlistのL9, L10の比較ではlistの方が高速です。要素を1つずつ頻繁に書き換えるような作業はlistの方が速いようです。
- copy.deepcopy()は、NumPyもlistも次元数の違いによる計算速度の違いがほとんどありません。
- N11とN10の計算速度の差、L10とL9の計算速度の差が、要素がゼロの配列を生成するのにかかる時間ですが、listよりもNumPyの方が明らかに速いです。
4.(参考資料)検証コード全文
いかがだったでしょうか。実際に複製にかかる時間を計測すると、どのようにコードを書くべきかが見えてくるのではないでしょうか。
参考資料として検証コードの全文を以下に紹介します。クリックするとコードが開きます。だらだらと長いコードなので参考資料としました。必要に応じて参考にしてください。
検証コード全文の表示・非表示の切り替え
※ブラウザによっては最初から表示されてしまいます。(Google Chrome推奨)
#コード02
import numpy as np
import time
import copy
import random
def f(x):
y = x #N1, L1
def f_slice_right(x):
y = x[:] #N2, L2
def f_slice_left(x, s):
s[:] = x #N3, N4, L3, L4
def f_np_array(x):
y = np.array(x) #N5
def f_list(x):
y = list(x) #L5
def f_np_copy(x):
y = np.copy(x) #N6
def f_copy(x):
y = x.copy() #N7, L6
def f_copy_copy(x):
y = copy.copy(x) #N8, L7
def f_deepcopy(x):
y = copy.deepcopy(x) #N9, L8
def f_for1(x, s, dim):
for i in range(dim):
s[i] = x[i] #N10, N11, L9, L10
def f_for2(x, s, dim1, dim2):
for i in range(dim1):
for j in range(dim2):
s[i][j] = x[i][j] #N10, N11, L9, L10
def f_for3(x, s, dim1, dim2, dim3):
for i in range(dim1):
for j in range(dim2):
for k in range(dim3):
s[i][j][k] = x[i][j][k] #N10, N11, L9, L10
def f_for2_np(x, s, dim1, dim2):
for i in range(dim1):
for j in range(dim2):
s[i, j] = x[i, j] #N12
def f_for3_np(x, s, dim1, dim2, dim3):
for i in range(dim1):
for j in range(dim2):
for k in range(dim3):
s[i, j, k] = x[i, j, k] #N12
n_dim = 1 #検証する次元の指定
nnn = 20000
print('nnn =',nnn)
if n_dim == 1:
#-----(1次元)-----
dim = 262144
print('dim =',dim)
x_np = np.random.rand(dim) #float64
x_list = [random.random() for i in range(dim)]
s_np = np.zeros(dim)
s_list = [0.0 for i in range(dim)]
elif n_dim == 2:
#-----(2次元)-----
dim1 = 512
dim2 = 512
print('dim1 =',dim1, 'dim2 =',dim2)
x_np = np.random.rand(dim1, dim2) #float64
x_list = [[random.random() for j in range(dim2)] \
for i in range(dim1)]
s_np = np.zeros((dim1, dim2))
s_list = [[0.0 for j in range(dim2)] for i in range(dim1)]
elif n_dim == 3:
#-----(3次元)-----
dim1 = 64
dim2 = 64
dim3 = 64
print('dim1 =',dim1, 'dim2 =',dim2, 'dim3 =',dim3)
x_np = np.random.rand(dim1, dim2, dim3) #float64
x_list = [[[random.random() for k in range(dim3)] \
for j in range(dim2)] for i in range(dim1)]
s_np = np.zeros((dim1, dim2, dim3))
s_list = [[[0.0 for k in range(dim3)] for j in range(dim2)] \
for i in range(dim1)]
#----------
print('===== NumPy配列のコピー =====')
print('[N1] y = x_np:')
start = time.time()
for i in range(nnn):
f(x_np)
print(time.time() - start)
print('[N2] s_np = x_np[:]:')
start = time.time()
for i in range(nnn):
f_slice_right(x_np)
print(time.time() - start)
print('[N3] s_np[:] = x_np:')
start = time.time()
for i in range(nnn):
f_slice_left(x_np, s_np)
print(time.time() - start)
print('[N4] s_np[:] = x_list:')
start = time.time()
for i in range(nnn):
f_slice_left(x_list, s_np)
print(time.time() - start)
print('[N5] y = np.array(x_list):')
start = time.time()
for i in range(nnn):
f_np_array(x_list)
print(time.time() - start)
print('[N6] y = np.copy(x_np):')
start = time.time()
for i in range(nnn):
f_np_copy(x_np)
print(time.time() - start)
print('[N7] y = x_np.copy():')
start = time.time()
for i in range(nnn):
f_copy(x_np)
print(time.time() - start)
print('[N8] y = copy.copy(x_np):')
start = time.time()
for i in range(nnn):
f_copy_copy(x_np)
print(time.time() - start)
print('[N9] y = copy.deepcopy(x_np):')
start = time.time()
for i in range(nnn):
f_deepcopy(x_np)
print(time.time() - start)
print('===== listのコピー =====')
print('[L1] y = x_list:')
start = time.time()
for i in range(nnn):
f(x_list)
print(time.time() - start)
print('[L2] s_list = x_list[:]:')
start = time.time()
for i in range(nnn):
f_slice_right(x_list)
print(time.time() - start)
print('[L3] s_list[:] = x_list:')
start = time.time()
for i in range(nnn):
f_slice_left(x_list, s_list)
print(time.time() - start)
print('[L4] s_list[:] = x_np:')
start = time.time()
for i in range(nnn):
f_slice_left(x_np, s_list)
print(time.time() - start)
print('[L5] y = list(x_np):')
start = time.time()
for i in range(nnn):
f_list(x_np)
print(time.time() - start)
print('[L6] y = x_list.copy():')
start = time.time()
for i in range(nnn):
f_copy(x_list)
print(time.time() - start)
print('[L7] y = copy.copy(x_list):')
start = time.time()
for i in range(nnn):
f_copy_copy(x_list)
print(time.time() - start)
print('[L8] y = copy.deepcopy(x_list):')
start = time.time()
for i in range(nnn):
f_deepcopy(x_list)
print(time.time() - start)
print('===== for文 =====')
if n_dim == 1:
print('[N10] for文_np, dim1:')
start = time.time()
for i in range(nnn):
f_for1(x_np, s_np, dim)
print(time.time() - start)
print('[L9] for文_list, dim1:')
start = time.time()
for i in range(nnn):
f_for1(x_list, s_list, dim)
print(time.time() - start)
elif n_dim == 2:
print('[N10]for文_np:, dim2')
start = time.time()
for i in range(nnn):
f_for2(x_np, s_np, dim1, dim2)
print(time.time() - start)
print('[N12] for文_np2:, dim2')
start = time.time()
for i in range(nnn):
f_for2_np(x_np, s_np, dim1, dim2)
print(time.time() - start)
print('[L9] for文_list, dim2:')
start = time.time()
for i in range(nnn):
f_for2(x_list, s_list, dim1, dim2)
print(time.time() - start)
elif n_dim == 3:
print('[N10] for文_np, dim3:')
start = time.time()
for i in range(nnn):
f_for3(x_np, s_np, dim1, dim2, dim3)
print(time.time() - start)
print('[N12] for文_np2, dim3:')
start = time.time()
for i in range(nnn):
f_for3_np(x_np, s_np, dim1, dim2, dim3)
print(time.time() - start)
print('[L9] for文_list, dim3:')
start = time.time()
for i in range(nnn):
f_for3(x_list, s_list, dim1, dim2, dim3)
print(time.time() - start)
print('===== for文(Sを初期化) =====')
if n_dim == 1:
print('[N11] for文_np, dim1:')
start = time.time()
for i in range(nnn):
s_np = np.zeros(dim)
f_for1(x_np, s_np, dim)
print(time.time() - start)
print('[L10] for文_list, dim1:')
start = time.time()
for i in range(nnn):
s_list = [0.0 for i in range(dim)]
f_for1(x_list, s_list, dim)
print(time.time() - start)
elif n_dim == 2:
print('[N11] for文_np:, dim2')
start = time.time()
for i in range(nnn):
s_np = np.zeros((dim1, dim2))
f_for2(x_np, s_np, dim1, dim2)
print(time.time() - start)
print('[N13] for文_np2:, dim2')
start = time.time()
for i in range(nnn):
s_np = np.zeros((dim1, dim2))
f_for2_np(x_np, s_np, dim1, dim2)
print(time.time() - start)
print('[L10] for文_list, dim2:')
start = time.time()
for i in range(nnn):
s_list = [[0.0 for j in range(dim2)] \
for i in range(dim1)]
f_for2(x_list, s_list, dim1, dim2)
print(time.time() - start)
elif n_dim == 3:
print('[N11] for文_np, dim3:')
start = time.time()
for i in range(nnn):
s_np = np.zeros((dim1, dim2, dim3))
f_for3(x_np, s_np, dim1, dim2, dim3)
print(time.time() - start)
print('[N13] for文_np2, dim3:')
start = time.time()
for i in range(nnn):
s_np = np.zeros((dim1, dim2, dim3))
f_for3_np(x_np, s_np, dim1, dim2, dim3)
print(time.time() - start)
print('[L10] for文_list, dim3:')
start = time.time()
for i in range(nnn):
s_list = [[[0.0 for k in range(dim3)] \
for j in range(dim2)] \
for i in range(dim1)]
f_for3(x_list, s_list, dim1, dim2, dim3)
print(time.time() - start)
私が実際に購入した教材のご紹介
以下、私が実際に購入したPythonの教材をまとめてみました。 Pythonを学習する上で、少しでもお役に立つことができればうれしいです。
・Python♪私が購入したPythonの書籍のレビュー
・UdemyのPythonの動画講座を書籍を買う感覚で購入してみた
その他
液晶ペンタブレットを買いました
(1) モバイルディスプレイを買うつもりだったのに激安ペンタブレット購入
以下、私が光回線を導入した時の記事一覧です。
(1) 2020年「光回線は値段で選ぶ」では後悔する ←宅内工事の状況も説明しています。
(2) NURO光の開通までWiFiルーターを格安レンタルできる
(3) NURO光の屋外工事の状況をご紹介。その日に開通!
(4) 光回線開通!実測するとNURO光はやっぱり速かった
(5) ネット上のNURO光紹介特典は個人情報がもれないの?