[...]ここでの私の[...]質問はnumpy.sum
、Python整数のリストで使用する方が、Python独自の整数を使用するよりも高速sum
でしょうか?
この質問に対する答えは次のとおりです。いいえ。
Pythonの合計はリストでより速くなり、NumPysの合計は配列でより速くなります。私は実際にタイミングを示すためにベンチマークを行いました(Python 3.6、NumPy 1.14):
import random
import numpy as np
import matplotlib.pyplot as plt
from simple_benchmark import benchmark
%matplotlib notebook
def numpy_sum(it):
return np.sum(it)
def python_sum(it):
return sum(it)
def numpy_sum_method(arr):
return arr.sum()
b_array = benchmark(
[numpy_sum, numpy_sum_method, python_sum],
arguments={2**i: np.random.randint(0, 10, 2**i) for i in range(2, 21)},
argument_name='array size',
function_aliases={numpy_sum: 'numpy.sum(<array>)', numpy_sum_method: '<array>.sum()', python_sum: "sum(<array>)"}
)
b_list = benchmark(
[numpy_sum, python_sum],
arguments={2**i: [random.randint(0, 10) for _ in range(2**i)] for i in range(2, 21)},
argument_name='list size',
function_aliases={numpy_sum: 'numpy.sum(<list>)', python_sum: "sum(<list>)"}
)
これらの結果で:
f, (ax1, ax2) = plt.subplots(1, 2, sharey=True)
b_array.plot(ax=ax1)
b_list.plot(ax=ax2)

左:NumPyアレイ上。右:Pythonリスト上。ベンチマークは非常に広範囲の値をカバーしているため、これは両対数プロットであることに注意してください。ただし、定性的な結果の場合:低いほど良いことを意味します。
これは、リストの場合、Pythonsum
が常に高速であるのに対し、配列np.sum
のsum
メソッドが高速になることを示しています(Pythonが高速である非常に短い配列を除くsum
)。
これらを互いに比較することに興味がある場合に備えて、私はそれらすべてを含むプロットも作成しました。
f, ax = plt.subplots(1)
b_array.plot(ax=ax)
b_list.plot(ax=ax)
ax.grid(which='both')

興味深いことに、numpy
Pythonやリストと配列で競合できるポイントは、およそ200要素です。この数は、Python / NumPyのバージョンなど、多くの要因に依存する可能性があることに注意してください...文字通りに解釈しすぎないでください。
言及されていないのは、この違いの理由です(つまり、関数が単に異なる一定のオーバーヘッドを持つ短いリスト/配列の違いではなく、大規模な違いを意味します)。CPythonを想定すると、PythonリストはPythonオブジェクト(この場合はPython整数)へのポインターのC(言語C)配列のラッパーです。これらの整数は、C整数のラッパーと見なすことができます(Python整数は任意に大きくなる可能性があるため、1つのC整数を単純に使用することはできませんが、十分に近いため、実際には正しくありません)。
たとえば、次のようなリスト[1, 2, 3]
が(概略的には、いくつかの詳細を省略して)次のように保存されます。

ただし、NumPy配列は、C値を含むC配列のラッパーです(この場合int
、またはlong
32ビットまたは64ビットに依存し、オペレーティングシステムに依存します)。
したがって、NumPy配列は次のようnp.array([1, 2, 3])
になります。

次に理解すべきことは、これらの関数がどのように機能するかです。
- Python
sum
は、反復可能ファイル(この場合はリストまたは配列)を反復処理し、すべての要素を追加します。
- NumPys
sum
メソッドは、格納されているC配列を反復処理し、これらのC値を追加し、最後にその値をPython型(この場合はnumpy.int32
(またはnumpy.int64
))でラップして返します。
- NumPys
sum
関数は、入力をarray
(少なくともまだ配列でない場合は)に変換してから、NumPysum
メソッドを使用します。
明らかに、C配列からC値を追加する方が、Pythonオブジェクトを追加するよりもはるかに高速です。そのため、NumPy関数ははるかに高速になります(上記の2番目のプロットを参照してください。配列のNumPy関数は、大きな配列のPythonの合計をはるかに上回ります)。
ただし、PythonリストをNumPy配列に変換するのは比較的遅いため、C値を追加する必要があります。これが、リストの場合、Pythonsum
が高速になる理由です。
残っている唯一の未解決の質問は、なぜPythonsum
がarray
非常に遅いのかということです(比較されたすべての関数の中で最も遅い)。そして、それは実際には、Pythonの合計が渡されたものを単純に繰り返すという事実と関係があります。リストの場合は格納されたPythonオブジェクトを取得しますが、1D NumPy配列の場合は格納されたPythonオブジェクトはなく、C値のみです。そのため、Python&NumPyは要素ごとにPythonオブジェクト(numpy.int32
または)を作成する必要があり、次にこれらのPythonオブジェクトを追加する必要があります。numpy.int64
C値のラッパーを作成すると、処理が非常に遅くなります。
さらに、Python整数とスカラーnumpy.int32を使用することの意味(パフォーマンスを含む)は何ですか?たとえば、+ = 1の場合、aのタイプがPython整数またはnumpy.int32の場合、動作やパフォーマンスに違いはありますか?
私はいくつかのテストを行いました。スカラーの加算と減算については、Pythonの整数に固執する必要があります。キャッシングが行われている可能性がありますが、これは、次のテストが完全に代表的ではない可能性があることを意味します。
from itertools import repeat
python_integer = 1000
numpy_integer_32 = np.int32(1000)
numpy_integer_64 = np.int64(1000)
def repeatedly_add_one(val):
for _ in repeat(None, 100000):
_ = val + 1
%timeit repeatedly_add_one(python_integer)
3.7 ms ± 71.2 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
%timeit repeatedly_add_one(numpy_integer_32)
14.3 ms ± 162 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
%timeit repeatedly_add_one(numpy_integer_64)
18.5 ms ± 494 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
def repeatedly_sub_one(val):
for _ in repeat(None, 100000):
_ = val - 1
%timeit repeatedly_sub_one(python_integer)
3.75 ms ± 236 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
%timeit repeatedly_sub_one(numpy_integer_32)
15.7 ms ± 437 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
%timeit repeatedly_sub_one(numpy_integer_64)
19 ms ± 834 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
Python整数を使用したスカラー演算は、NumPyスカラーを使用した場合よりも3〜6倍高速です。なぜそうなるのかは確認していませんが、NumPyスカラーはめったに使用されず、おそらくパフォーマンスが最適化されていないのではないかと思います。
両方のオペランドがnumpyスカラーである算術演算を実際に実行すると、違いは少し少なくなります。
def repeatedly_add_one(val):
one = type(val)(1) # create a 1 with the same type as the input
for _ in repeat(None, 100000):
_ = val + one
%timeit repeatedly_add_one(python_integer)
3.88 ms ± 273 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
%timeit repeatedly_add_one(numpy_integer_32)
6.12 ms ± 324 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
%timeit repeatedly_add_one(numpy_integer_64)
6.49 ms ± 265 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
その後、2倍遅くなります。
itertools.repeat
代わりに単純に使用できたのに、なぜここで使用したのか疑問に思われるかもしれませんfor _ in range(...)
。その理由は、repeat
より高速であるため、ループあたりのオーバーヘッドが少なくなるためです。私は加算/減算時間にのみ関心があるので、実際には、ループするオーバーヘッドがタイミングを乱さないことが望ましいです(少なくともそれほど多くはありません)。