web-dev-qa-db-ja.com

Pythonの配列が遅いのはなぜですか?

配列がボックス化されていないように見えるため、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

そのような違いの原因は何でしょうか?

146

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]);
}

生メモリは、プラットフォーム固有のClong整数のベクトルとして扱われます。 i 'th C longが読み込まれます。 PyLong_FromLong()が呼び出されて、ネイティブのC longをPython longオブジェクト(Python 3 Python 2のintlongの区別を排除し、実際にはint型として表示されます。

このボクシングでは、Python intオブジェクトに新しいメモリを割り当て、ネイティブC longのビットをそのオブジェクトにスプレーする必要があります。元の例のコンテキストでは、このオブジェクトの存続期間は非常に短く(sum()が現在の合計にコンテンツを追加するのに十分な長さ)、新しいintオブジェクトの割り当てを解除するのにより多くの時間が必要です。 。

これは、CPython実装において速度の違いが発生する場所であり、常に発生し、常に発生する場所です。

210
Tim Peters

Tim Petersの優れた答えに追加するために、配列は buffer protocol を実装しますが、リストは実装しません。これは、C拡張機能(または Cython モジュールの作成などの道徳的な同等物)を作成している場合、アクセスして作業できることを意味します配列の要素はPythonができることよりもはるかに高速です。これにより、速度が大幅に向上し、場合によっては桁違いに向上します。ただし、多くの欠点があります。

  1. 現在、Pythonの代わりにCを作成しています。 Cythonはこれを改善する1つの方法ですが、言語間の多くの根本的な違いを排除するものではありません。 Cのセマンティクスに精通し、Cのセマンティクスを理解する必要があります。
  2. PyPyのC APIは ある程度 で動作しますが、非常に高速ではありません。 PyPyをターゲットにしている場合は、通常のリストを使用して単純なコードを作成し、JITterに最適化させてください。
  3. C拡張は、コンパイルする必要があるため、純粋なPythonコードよりも配布が困難です。コンパイルはアーキテクチャとオペレーティングシステムに依存する傾向があるため、ターゲットプラットフォーム用にコンパイルしていることを確認する必要があります。

ユースケースによっては、C拡張機能に直接進むと、ハンマーを使用してフライを叩くことがあります。最初に NumPy を調査し、実行しようとしている数学が何でもできるほど強力かどうかを確認する必要があります。正しく使用すれば、ネイティブPythonよりもはるかに高速になります。

82
Kevin

ティムピーターズは答えましたなぜこれは遅いですが、改善する方法それを見てみましょう。

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ビット整数よりも速いため、メモリ/帯域幅の制限がある場合にのみ価値があります。

8
Robin Roth