要素のインスタンス化と取得に関して、タプルとリストの間にパフォーマンスの違いはありますか?
9 に答える
概要
タプルは、ほぼすべてのカテゴリでリストよりも優れたパフォーマンスを発揮する傾向があります。
1) タプルは一定に折りたたむことができます。
2) タプルはコピーする代わりに再利用できます。
3) タプルはコンパクトであり、過度に割り当てません。
4) タプルは要素を直接参照します。
タプルは常に折りたたむことができます
定数のタプルは、Python のピープホール オプティマイザーまたは AST オプティマイザーによって事前計算できます。一方、リストはゼロから構築されます。
>>> from dis import dis
>>> dis(compile("(10, 'abc')", '', 'eval'))
1 0 LOAD_CONST 2 ((10, 'abc'))
3 RETURN_VALUE
>>> dis(compile("[10, 'abc']", '', 'eval'))
1 0 LOAD_CONST 0 (10)
3 LOAD_CONST 1 ('abc')
6 BUILD_LIST 2
9 RETURN_VALUE
タプルをコピーする必要はありません
実行中tuple(some_tuple)
はすぐに戻ります。タプルは不変であるため、コピーする必要はありません。
>>> a = (10, 20, 30)
>>> b = tuple(a)
>>> a is b
True
対照的に、list(some_list)
すべてのデータを新しいリストにコピーする必要があります。
>>> a = [10, 20, 30]
>>> b = list(a)
>>> a is b
False
タプルは過剰に割り当てない
タプルのサイズは固定されているため、 append()操作を効率化するために過剰に割り当てる必要があるリストよりもコンパクトに格納できます。
これにより、タプルは優れたスペースの利点を得ることができます:
>>> import sys
>>> sys.getsizeof(tuple(iter(range(10))))
128
>>> sys.getsizeof(list(iter(range(10))))
200
これは、リストが何をしているかを説明するObjects/listobject.cからのコメントです。
/* This over-allocates proportional to the list size, making room
* for additional growth. The over-allocation is mild, but is
* enough to give linear-time amortized behavior over a long
* sequence of appends() in the presence of a poorly-performing
* system realloc().
* The growth pattern is: 0, 4, 8, 16, 25, 35, 46, 58, 72, 88, ...
* Note: new_allocated won't overflow because the largest possible value
* is PY_SSIZE_T_MAX * (9 / 8) + 6 which always fits in a size_t.
*/
タプルはその要素を直接参照します
オブジェクトへの参照は、タプル オブジェクトに直接組み込まれます。対照的に、リストには、ポインターの外部配列への間接的な追加レイヤーがあります。
これにより、タプルはインデックス付きルックアップとアンパックの速度がわずかに向上します。
$ python3.6 -m timeit -s 'a = (10, 20, 30)' 'a[1]'
10000000 loops, best of 3: 0.0304 usec per loop
$ python3.6 -m timeit -s 'a = [10, 20, 30]' 'a[1]'
10000000 loops, best of 3: 0.0309 usec per loop
$ python3.6 -m timeit -s 'a = (10, 20, 30)' 'x, y, z = a'
10000000 loops, best of 3: 0.0249 usec per loop
$ python3.6 -m timeit -s 'a = [10, 20, 30]' 'x, y, z = a'
10000000 loops, best of 3: 0.0251 usec per loop
タプルの(10, 20)
格納方法は次のとおりです。
typedef struct {
Py_ssize_t ob_refcnt;
struct _typeobject *ob_type;
Py_ssize_t ob_size;
PyObject *ob_item[2]; /* store a pointer to 10 and a pointer to 20 */
} PyTupleObject;
リストの[10, 20]
保存方法は次のとおりです。
PyObject arr[2]; /* store a pointer to 10 and a pointer to 20 */
typedef struct {
Py_ssize_t ob_refcnt;
struct _typeobject *ob_type;
Py_ssize_t ob_size;
PyObject **ob_item = arr; /* store a pointer to the two-pointer array */
Py_ssize_t allocated;
} PyListObject;
タプル オブジェクトには 2 つのデータ ポインターが直接組み込まれているのに対し、リスト オブジェクトには 2 つのデータ ポインターを保持する外部配列への間接レイヤーが追加されていることに注意してください。
一般に、タプルはわずかに高速であると期待できます。ただし、特定のケースを確実にテストする必要があります(違いがプログラムのパフォーマンスに影響を与える可能性がある場合は、「時期尚早の最適化がすべての悪の根源である」ことを忘れないでください)。
Pythonはこれを非常に簡単にします:timeitはあなたの友達です。
$ python -m timeit "x=(1,2,3,4,5,6,7,8)"
10000000 loops, best of 3: 0.0388 usec per loop
$ python -m timeit "x=[1,2,3,4,5,6,7,8]"
1000000 loops, best of 3: 0.363 usec per loop
と...
$ python -m timeit -s "x=(1,2,3,4,5,6,7,8)" "y=x[3]"
10000000 loops, best of 3: 0.0938 usec per loop
$ python -m timeit -s "x=[1,2,3,4,5,6,7,8]" "y=x[3]"
10000000 loops, best of 3: 0.0649 usec per loop
したがって、この場合、インスタンス化はタプルの場合はほぼ1桁速くなりますが、リストの場合、アイテムへのアクセスは実際にはいくらか速くなります。したがって、いくつかのタプルを作成して何度もアクセスする場合は、代わりにリストを使用する方が実際には高速である可能性があります。
もちろん、アイテムを変更したい場合は、アイテムの1つを変更するためにまったく新しいタプルを作成する必要があるため、リストは間違いなく高速になります(タプルは不変であるため)。
このdis
モジュールは、関数のバイトコードを分解し、タプルとリストの違いを確認するのに役立ちます。
この場合、要素にアクセスすると同じコードが生成されますが、タプルの割り当てはリストの割り当てよりもはるかに高速であることがわかります。
>>> def a():
... x=[1,2,3,4,5]
... y=x[2]
...
>>> def b():
... x=(1,2,3,4,5)
... y=x[2]
...
>>> import dis
>>> dis.dis(a)
2 0 LOAD_CONST 1 (1)
3 LOAD_CONST 2 (2)
6 LOAD_CONST 3 (3)
9 LOAD_CONST 4 (4)
12 LOAD_CONST 5 (5)
15 BUILD_LIST 5
18 STORE_FAST 0 (x)
3 21 LOAD_FAST 0 (x)
24 LOAD_CONST 2 (2)
27 BINARY_SUBSCR
28 STORE_FAST 1 (y)
31 LOAD_CONST 0 (None)
34 RETURN_VALUE
>>> dis.dis(b)
2 0 LOAD_CONST 6 ((1, 2, 3, 4, 5))
3 STORE_FAST 0 (x)
3 6 LOAD_FAST 0 (x)
9 LOAD_CONST 2 (2)
12 BINARY_SUBSCR
13 STORE_FAST 1 (y)
16 LOAD_CONST 0 (None)
19 RETURN_VALUE
タプルは不変であるため、メモリ効率が高くなります。リスト、速度効率のために、メモリを過剰に割り当てて、定数なしで追加できるようにしrealloc
ます。そのため、コード内の値の一定のシーケンスを反復処理する場合 (例: for direction in 'up', 'right', 'down', 'left':
)、タプルが優先されます。タプルはコンパイル時に事前に計算されるためです。
読み取りアクセス速度は同じである必要があります (どちらも連続した配列としてメモリに格納されます)。
ただし、変更可能なデータを扱う場合alist.append(item)
よりもはるかに好まれます。atuple+= (item,)
タプルは、フィールド名のないレコードとして扱われることを意図していることを思い出してください。
念のため、もう 1 つの小さなベンチマークを示します。
In [11]: %timeit list(range(100))
749 ns ± 2.41 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
In [12]: %timeit tuple(range(100))
781 ns ± 3.34 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
In [1]: %timeit list(range(1_000))
13.5 µs ± 466 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
In [2]: %timeit tuple(range(1_000))
12.4 µs ± 182 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
In [7]: %timeit list(range(10_000))
182 µs ± 810 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
In [8]: %timeit tuple(range(10_000))
188 µs ± 2.38 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
In [3]: %timeit list(range(1_00_000))
2.76 ms ± 30.5 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
In [4]: %timeit tuple(range(1_00_000))
2.74 ms ± 31.8 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
In [10]: %timeit list(range(10_00_000))
28.1 ms ± 266 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
In [9]: %timeit tuple(range(10_00_000))
28.5 ms ± 447 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
これらを平均してみましょう:
In [3]: l = np.array([749 * 10 ** -9, 13.5 * 10 ** -6, 182 * 10 ** -6, 2.76 * 10 ** -3, 28.1 * 10 ** -3])
In [2]: t = np.array([781 * 10 ** -9, 12.4 * 10 ** -6, 188 * 10 ** -6, 2.74 * 10 ** -3, 28.5 * 10 ** -3])
In [11]: np.average(l)
Out[11]: 0.0062112498000000006
In [12]: np.average(t)
Out[12]: 0.0062882362
In [17]: np.average(t) / np.average(l) * 100
Out[17]: 101.23946713590554
ほとんど決定的ではないと言えます。
しかし確かに、タプルはリストに比べて仕事をするのに101.239%
時間がかかりました。1.239%
array
リストまたはタプルのすべての項目が同じ C 型である場合は、標準ライブラリのモジュールも考慮する必要があります。必要なメモリが少なくなり、高速になる可能性があります。
タプルは不変であるため、リストよりもわずかに効率的であり、そのため高速です。