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


153

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

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


4
numpyツールはアレイを効率的に活用できます:%timeit np.sum(A):100ループ、3:8.87 msが最高
BM

6
arrayパッケージを使用する必要がある状況に出くわしたことはありません。大量の計算を実行したい場合、Numpyは高速(つまりC)で動作し、通常、のような単純な実装よりも優れていsum()ます。
Nick T

40
近い有権者:なぜこれが正確に意見に基づいているのですか?OPは、測定可能で再現可能な現象について、特定の技術的な質問をしているようです。
ケビン

5
@NickT読み取り最適化の逸話array整数の文字列(ASCIIバイトを表す)をstrオブジェクトに変換するのは非常に高速です。グイド自身は他の多くの解決策の後にのみこれを思い付き、パフォーマンスにかなり驚いていました。とにかく、これが私がそれが有用であると思ったのを覚えている唯一の場所です。numpy配列の処理にははるかに優れていますが、これはサードパーティの依存関係です。
Bakuriu

回答:


220

ストレージは、「ボックス化解除」ですが、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です)。

そして、型コードの__getitem__実装は次のとおりarrayですl

static PyObject *
l_getitem(arrayobject *ap, Py_ssize_t i)
{
    return PyLong_FromLong(((long *)ap->ob_item)[i]);
}

rawメモリは、プラットフォーム固有のC long整数のベクトルとして扱われます。i「目をC long読んされます。そして次にPyLong_FromLong()(「ボックス」)ネイティブラップするために呼び出されC longたPythonにおけるlongオブジェクト(これは、間のPython 2の区別をなくすのPython 3にint及びlong、実際型として示されていますint)。

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

これは、速度の違いが原因であり、常に原因であり、CPython実装で常に原因になります。


87

Tim Petersの優れた答えに追加するために、アレイはバッファプロトコルを実装しています、リストはません。つまり、C拡張機能(またはCythonモジュールを作成するなど、道徳的に同等のもの)を作成している場合、Pythonが実行できるよりもはるかに高速に配列の要素にアクセスして操作できます。これにより、速度が大幅に向上し、おそらく1桁以上向上します。ただし、いくつかの欠点があります。

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

ユースケースによっては、Cエクステンションに直行する場合、大ハンマーを使用してフライを叩く場合があります。最初にNumPyを調べて、計算しようとしている数学を実行するのに十分強力かどうかを確認する必要があります。正しく使用すれば、ネイティブPythonよりもはるかに高速になります。


10

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

あなたの例にこだわる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

したがって、リストソリューションから派手なバージョンまで、これは実行時に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ビット整数よりも高速であるため、メモリ/帯域幅に制限がある場合にのみ価値があります。


-1

100000000等しく10^8ないことに注意してください10^7、そして私の結果は次のとおりです:

100000000 == 10**8

# my test results on a Linux virtual machine:
#<L = list(range(100000000))> Time: 0:00:03.263585
#<A = array.array('l', range(100000000))> Time: 0:00:16.728709
#<L = list(range(10**8))> Time: 0:00:03.119379
#<A = array.array('l', range(10**8))> Time: 0:00:18.042187
#<A = array.array('l', L)> Time: 0:00:07.524478
#<sum(L)> Time: 0:00:01.640671
#<np.sum(L)> Time: 0:00:20.762153
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.