Python♪FEM:配列の次元数別の複製速度(list、NumPy配列)

list、NumPy配列の1~3次元配列について、参照渡し、浅いコピー、深いコピーなどの複製時間を計測しました。速度の差を感覚的に知っておくことは重要だと思います。私自身、予想外の計測結果になったものもあり、楽しむことができました。

0. FEMなど数値解析シリーズ

この記事は「FEMなど数値解析シリーズ」の記事です。一連の記事は、以下のリンク集を参照してください。

Python♪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次元
N1y = x_np参照渡し0.0020.0020.002
N2y = x_np[:]要素の参照渡し0.0050.0050.005
N3s_np[:] = x_np要素の値コピー1.7502.5622.158
N4s_np[:] = x_listlist→NumPy96.66195.804114.639
N5y = np.array(x_list)list→NumPy114.534111.620127.565
N6y = np.copy(x_np)(浅い)コピー18.27818.00118.339
N7y = x_np.copy()(浅い)コピー18.17518.27018.020
N8y = copy.copy(x_np)(浅い)コピー18.09518.34017.707
N9y = copy.deepcopy(x_np)(深い)コピー18.21118.16517.576
N10要素毎のコピー(for文)
s[i][j][k] = x[i][j][k]
要素のコピー707.4202057.2533186.018
N11N10のs[]を毎回初期化要素のコピー725.5982050.1313236.215
N12要素毎のコピー(for文)
s[i, j, k] = x[i, j, k]
要素のコピー707.420
(=N10)
1078.5631299.485
N13N12のs[]を毎回初期化要素のコピー725.598
(=N11)
1094.6201308.806

下表は各次元のN1の計算時間を1としたときのN2~N11の計算時間の割合を示したものです。例えばN2の計算時間はN1の3倍であることがわかります。

複製の方法備考1次元2次元3次元
N1y = x_np参照渡し111
N2y = x_np[:]要素の参照渡し333
N3s_np[:] = x_np要素の値コピー87312781087
N4s_np[:] = x_listlist→NumPy482074777557751
N5y = np.array(x_list)list→NumPy571215566164262
N6y = np.copy(x_np)(浅い)コピー911689769239
N7y = x_np.copy()(浅い)コピー906491119078
N8y = copy.copy(x_np)(浅い)コピー902591458920
N9y = copy.deepcopy(x_np)(深い)コピー908290588854
N10要素毎のコピー(for文)
s[i][j][k] = x[i][j][k]
要素のコピー35281010258881604988
N11N10のs[]を毎回初期化要素のコピー36187610223361630275
N12 要素毎のコピー(for文)
s[i, j, k] = x[i, j, k]
要素のコピー352810
(=N10)
537846654628
N13 N12のs[]を毎回初期化 要素のコピー361876
(=N11)
545853659324

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次元
L1y = x_list参照渡し0.0020.0020.002
L2y = x_list[:]浅いコピー43.8790.0280.006
L3s_list[:] = x_list浅いコピー56.1750.0270.008
L4s_list[:] = x_npNumPy→list268.9261.2630.176
L5y = list(x_np)NumPy→list206.5361.2450.185
L6y = x_list.copy()浅いコピー44.3920.0290.007
L7y = copy.copy(x_list)浅いコピー45.3070.0390.013
L8y = copy.deepcopy(x_list)深いコピー2975.2453001.6223231.843
L9要素毎のコピー(for文)要素のコピー292.382387.693519.536
L10N10の代入先を毎回初期化要素のコピー523.983554.042725.232

下表は各次元のL1の計算時間を1としたときのL2~L10の計算時間の割合を示します。

複製の方法備考1次元2次元3次元
L1y = x_list参照渡し111
L2y = x_list[:]浅いコピー21972143
L3s_list[:] = x_list浅いコピー28130134
L4s_list[:] = x_npNumPy→list13466663087
L5y = list(x_np)NumPy→list10342462191
L6y = x_list.copy()浅いコピー22230153
L7y = copy.copy(x_list)浅いコピー22687196
L8y = copy.deepcopy(x_list)深いコピー148986214966371591001
L9要素毎のコピー(for文)要素のコピー146411193307255762
L10N10の代入先を毎回初期化要素のコピー262386276250357024

主なポイントは以下の通りです。

  • 浅いコピー(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の動画講座を書籍を買う感覚で購入してみた

その他

Twitterへのリンクです。SNSもはじめました♪

液晶ペンタブレットを買いました
 (1) モバイルディスプレイを買うつもりだったのに激安ペンタブレット購入

以下、私が光回線を導入した時の記事一覧です。
 (1) 2020年「光回線は値段で選ぶ」では後悔する ←宅内工事の状況も説明しています。
 (2) NURO光の開通までWiFiルーターを格安レンタルできる
 (3) NURO光の屋外工事の状況をご紹介。その日に開通!
 (4) 光回線開通!実測するとNURO光はやっぱり速かった
 (5) ネット上のNURO光紹介特典は個人情報がもれないの?