配列がボックス化されていないように見えるため、array.array
はリストよりも高速であると予想しました。
ただし、次の結果が得られます。
In [1]: import array
In [2]: L = list(range(100000000))
In [3]: A = array.array('l', range(100000000))
In [4]: %timeit sum(L)
1 loop, best of 3: 667 ms per loop
In [5]: %timeit sum(A)
1 loop, best of 3: 1.41 s per loop
In [6]: %timeit sum(L)
1 loop, best of 3: 627 ms per loop
In [7]: %timeit sum(A)
1 loop, best of 3: 1.39 s per loop
そのような違いの原因は何でしょうか?
storageは「unboxed」ですが、要素にアクセスするたびにPythonが「ボックス化」する必要があります(通常のPythonオブジェクトに埋め込みます)それで何でもするように。たとえば、sum(A)
は配列を反復処理し、通常のPython int
オブジェクトで各整数を1つずつ囲みます。それには時間がかかります。 sum(L)
では、リストが作成されたときにすべてのボクシングが行われました。
そのため、最終的には、配列は一般に低速ですが、必要なメモリは大幅に少なくなります。
これは、最近のバージョンのPython 3からの関連コードですが、Pythonが最初にリリースされて以来、同じ基本的な考え方がすべてのCPython実装に適用されます。
リストアイテムにアクセスするコードは次のとおりです。
PyObject *
PyList_GetItem(PyObject *op, Py_ssize_t i)
{
/* error checking omitted */
return ((PyListObject *)op) -> ob_item[i];
}
ほとんどありません:somelist[i]
はリスト内のi
'番目のオブジェクトを返します(そしてCPythonのすべてのPythonオブジェクトは、初期セグメントがレイアウトに適合する構造体へのポインターです。 struct PyObject
)。
そして、タイプコードarray
を持つl
の__getitem__
実装は次のとおりです。
static PyObject *
l_getitem(arrayobject *ap, Py_ssize_t i)
{
return PyLong_FromLong(((long *)ap->ob_item)[i]);
}
生メモリは、プラットフォーム固有のC
long
整数のベクトルとして扱われます。 i
'th C long
が読み込まれます。 PyLong_FromLong()
が呼び出されて、ネイティブのC long
をPython long
オブジェクト(Python 3 Python 2のint
とlong
の区別を排除し、実際にはint
型として表示されます。
このボクシングでは、Python int
オブジェクトに新しいメモリを割り当て、ネイティブC long
のビットをそのオブジェクトにスプレーする必要があります。元の例のコンテキストでは、このオブジェクトの存続期間は非常に短く(sum()
が現在の合計にコンテンツを追加するのに十分な長さ)、新しいint
オブジェクトの割り当てを解除するのにより多くの時間が必要です。 。
これは、CPython実装において速度の違いが発生する場所であり、常に発生し、常に発生する場所です。
Tim Petersの優れた答えに追加するために、配列は buffer protocol を実装しますが、リストは実装しません。これは、C拡張機能(または Cython モジュールの作成などの道徳的な同等物)を作成している場合、アクセスして作業できることを意味します配列の要素はPythonができることよりもはるかに高速です。これにより、速度が大幅に向上し、場合によっては桁違いに向上します。ただし、多くの欠点があります。
ユースケースによっては、C拡張機能に直接進むと、ハンマーを使用してフライを叩くことがあります。最初に NumPy を調査し、実行しようとしている数学が何でもできるほど強力かどうかを確認する必要があります。正しく使用すれば、ネイティブPythonよりもはるかに高速になります。
ティムピーターズは答えましたなぜこれは遅いですが、改善する方法それを見てみましょう。
sum(range(...))
の例にこだわる(ここでのメモリに収まるように、例よりも10倍小さい):
import numpy
import array
L = list(range(10**7))
A = array.array('l', L)
N = numpy.array(L)
%timeit sum(L)
10 loops, best of 3: 101 ms per loop
%timeit sum(A)
1 loop, best of 3: 237 ms per loop
%timeit sum(N)
1 loop, best of 3: 743 ms per loop
また、この方法では、numpyはボックス化/アンボックス化する必要があり、追加のオーバーヘッドがあります。高速にするには、numpy cコード内にとどまらなければなりません:
%timeit N.sum()
100 loops, best of 3: 6.27 ms per loop
したがって、リストソリューションからnumpyバージョンまで、これは実行時の16倍になります。
これらのデータ構造の作成にかかる時間も確認しましょう
%timeit list(range(10**7))
1 loop, best of 3: 283 ms per loop
%timeit array.array('l', range(10**7))
1 loop, best of 3: 884 ms per loop
%timeit numpy.array(range(10**7))
1 loop, best of 3: 1.49 s per loop
%timeit numpy.arange(10**7)
10 loops, best of 3: 21.7 ms per loop
明確な勝者:Numpy
また、データ構造の作成には、合計と同じくらいの時間がかかります。メモリの割り当てが遅い。
それらのメモリ使用量:
sys.getsizeof(L)
90000112
sys.getsizeof(A)
81940352
sys.getsizeof(N)
80000096
そのため、これらはさまざまなオーバーヘッドで数ごとに8バイトかかります。 32ビット整数を使用する範囲では十分なので、メモリをいくらか安全にできます。
N=numpy.arange(10**7, dtype=numpy.int32)
sys.getsizeof(N)
40000096
%timeit N.sum()
100 loops, best of 3: 8.35 ms per loop
しかし、私のマシンでは64ビット整数の追加は32ビット整数よりも速いため、メモリ/帯域幅の制限がある場合にのみ価値があります。