it-swarm.com.de

Warum ist Tuple in Python schneller als list?

Ich habe gerade in "Dive into Python" gelesen, dass "Tupel schneller sind als Listen".

Tupel ist unveränderlich und Liste ist veränderlich, aber ich verstehe nicht ganz, warum Tupel schneller ist.

Hat jemand einen Leistungstest dazu gemacht?

53
Vimvq1987

Das angegebene Verhältnis "Konstruktionsgeschwindigkeit" gilt nur für Konstante Tupel (solche, deren Elemente in Litern ausgedrückt werden). Beachten Sie sorgfältig (und wiederholen Sie dies auf Ihrem Computer - Sie müssen nur die Befehle in einem Shell/Befehlsfenster eingeben!) ...:

$ python3.1 -mtimeit -s'x,y,z=1,2,3' '[x,y,z]'
1000000 loops, best of 3: 0.379 usec per loop
$ python3.1 -mtimeit '[1,2,3]'
1000000 loops, best of 3: 0.413 usec per loop

$ python3.1 -mtimeit -s'x,y,z=1,2,3' '(x,y,z)'
10000000 loops, best of 3: 0.174 usec per loop
$ python3.1 -mtimeit '(1,2,3)'
10000000 loops, best of 3: 0.0602 usec per loop

$ python2.6 -mtimeit -s'x,y,z=1,2,3' '[x,y,z]'
1000000 loops, best of 3: 0.352 usec per loop
$ python2.6 -mtimeit '[1,2,3]'
1000000 loops, best of 3: 0.358 usec per loop

$ python2.6 -mtimeit -s'x,y,z=1,2,3' '(x,y,z)'
10000000 loops, best of 3: 0.157 usec per loop
$ python2.6 -mtimeit '(1,2,3)'
10000000 loops, best of 3: 0.0527 usec per loop

Ich habe die Messungen nicht mit 3.0 gemacht, weil ich sie natürlich nicht in der Nähe habe - sie ist absolut veraltet und es gibt absolut keinen Grund, sie zu behalten, da 3.1 in jeder Hinsicht überlegen ist (Python 2.7, wenn Sie es tun) Das Upgrade ist in jeder Aufgabe fast 20% schneller als 2,6 - und 2,6 ist, wie Sie sehen, schneller als 3,1 -. Wenn Sie also die Leistung ernst nehmen, ist Python 2.7 wirklich das einzige Release, das Sie sollten los sein!).

Der Schlüsselpunkt dabei ist jedoch, dass das Erstellen einer Liste aus konstanten Literalen in jeder Python-Version etwa gleich schnell oder etwas langsamer ist als das Erstellen von Listen, deren Werte durch Variablen referenziert werden. Tupel verhalten sich jedoch ganz anders - ein Tuple aus konstanten Literalen zu bauen ist normalerweise dreimal so schnell wie aus Werten, auf die Variablen verweisen. Sie fragen sich vielleicht, wie das sein kann, oder? -)

Antwort: Ein Tuple aus konstanten Literalen kann vom Python-Compiler leicht als ein unveränderlicher Konstantenliteral identifiziert werden: er wird also im Wesentlichen nur einmal erstellt, wenn der Compiler die Quelle in Bytecodes umwandelt und in der "constants" -Tabelle versteckt "der jeweiligen Funktion oder des Moduls. Wenn diese Bytecodes ausgeführt werden, müssen sie nur die vordefinierte Konstante Tuple - hey presto! -) wiederherstellen.

Diese einfache Optimierung kann nicht auf Listen angewendet werden, da eine Liste ein veränderbares Objekt ist. Wenn also derselbe Ausdruck wie [1, 2, 3] zweimal ausgeführt wird (in einer Schleife - das Modul timeit macht die Schleife für Sie ;-), Ein neues, neues Listenobjekt wird jedes Mal neu erstellt - und diese Konstruktion (wie die Konstruktion eines Tuples, wenn der Compiler es nicht trivial als Konstante für die Kompilierzeit und als unveränderliches Objekt identifizieren kann) dauert etwas Zeit.

Allerdings ist die Tupel-Konstruktion (wenn beide Konstruktionen tatsächlich Auftreten müssen) immer noch etwa doppelt so schnell wie die Listenkonstruktion - und dass Diskrepanz durch die Einfachheit der Tuple erklärt werden kann Andere Antworten wurden wiederholt erwähnt. Diese Einfachheit ist jedoch nicht für eine Beschleunigung um das Sechsfache oder mehr verantwortlich, wie Sie feststellen, wenn Sie nur die Konstruktion von Listen und Tupeln mit einfachen konstanten Literalen als Gegenständen vergleichen! _)

80
Alex Martelli

Mit dem Modul timeit können Sie häufig leistungsbezogene Fragen selbst lösen:

$ python2.6 -mtimeit -s 'a = Tuple(range(10000))' 'for i in a: pass'
10000 loops, best of 3: 189 usec per loop
$ python2.6 -mtimeit -s 'a = list(range(10000))' 'for i in a: pass' 
10000 loops, best of 3: 191 usec per loop

Dies zeigt, dass Tuple vernachlässigbar schneller als die Iterationsliste ist. Ich erhalte ähnliche Ergebnisse für die Indizierung, aber für den Aufbau zerstört Tuple die Liste:

$ python2.6 -mtimeit '(1, 2, 3, 4)'   
10000000 loops, best of 3: 0.0266 usec per loop
$ python2.6 -mtimeit '[1, 2, 3, 4]'
10000000 loops, best of 3: 0.163 usec per loop

Wenn also die Geschwindigkeit der Iteration oder des Indexierens die einzigen Faktoren sind, gibt es praktisch keinen Unterschied, aber für die Konstruktion gewinnen Tupel.

16
Alec Thomas

Alex gab eine großartige Antwort, aber ich werde versuchen, ein paar Dinge zu erweitern, die ich für erwähnenswert halte. Alle Leistungsunterschiede sind im Allgemeinen klein und implementierungsspezifisch. Setzen Sie die Farm also nicht darauf.

In CPython werden Tupel in einem einzelnen Speicherblock gespeichert, sodass das Erstellen eines neuen Tupels im schlimmsten Fall einen einzelnen Aufruf zum Zuweisen von Speicher erfordert. Listen werden in zwei Blöcken zugeordnet: der feste mit allen Python-Objektinformationen und einem Block mit variabler Größe für die Daten. Dies ist ein Teil des Grunds, warum das Erstellen eines Tuples schneller ist, aber es erklärt wahrscheinlich auch den geringfügigen Unterschied in der Indizierungsgeschwindigkeit, da ein Zeiger weniger folgt.

Es gibt auch Optimierungen in CPython, um Speicherzuordnungen zu reduzieren: Nicht zugewiesene Listenobjekte werden in einer freien Liste gespeichert, sodass sie wiederverwendet werden können. Für die Zuordnung einer nicht leeren Liste ist jedoch noch eine Speicherzuordnung für die Daten erforderlich. Tupel werden auf 20 freien Listen für Tupel unterschiedlicher Größe gespeichert, sodass für die Zuweisung eines kleinen Tupels häufig keine Speicherzuweisungsaufrufe erforderlich sind.

Optimierungen wie diese sind in der Praxis hilfreich, aber sie machen es möglicherweise riskant, sich zu sehr auf die Ergebnisse von 'Timeit' zu verlassen, und natürlich sind sie völlig anders, wenn Sie zu IronPython wechseln, bei dem die Speicherzuweisung ganz anders funktioniert.

16
Duncan

Zusammenfassung

Tupel schneiden in fast jeder Kategorie besser ab als Listen :

1) Tupel können konstant gefaltet sein.

2) Tupel können wiederverwendet werden, anstatt sie zu kopieren.

3) Tupel sind kompakt und weisen keine übermäßigen Zuweisungen auf.

4) Tupel referenzieren ihre Elemente direkt.

Tupel können konstant gefaltet werden

Tupel von Konstanten können von Pythons Peephole-Optimierer oder AST-Optimierer vorberechnet werden. Auf der anderen Seite werden Listen von Grund auf neu aufgebaut:

    >>> 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 

Tupel müssen nicht kopiert werden

Das Ausführen von Tuple(some_Tuple) kehrt sofort selbst zurück. Da Tupel unveränderlich sind, müssen sie nicht kopiert werden:

>>> a = (10, 20, 30)
>>> b = Tuple(a)
>>> a is b
True

Im Gegensatz dazu erfordert list(some_list), dass alle Daten in eine neue Liste kopiert werden:

>>> a = [10, 20, 30]
>>> b = list(a)
>>> a is b
False

Tupel verteilen sich nicht

Da die Größe eines Tuples fest ist, kann er kompakter gespeichert werden als Listen, die eine Überbelegung erfordern, um append () operations effizient zu machen.

Dies gibt Tupeln einen schönen Raumvorteil:

>>> import sys
>>> sys.getsizeof(Tuple(iter(range(10))))
128
>>> sys.getsizeof(list(iter(range(10))))
200

Hier ist der Kommentar aus Objects/listobject.c , der erklärt, was Listen tun:

/* 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.
 */

Tupel verweisen direkt auf ihre Elemente

Verweise auf Objekte werden direkt in ein Tuple-Objekt eingefügt. Im Gegensatz dazu haben Listen eine zusätzliche Ebene der Indirektion zu einem externen Array von Zeigern.

Dies gibt Tupeln einen kleinen Geschwindigkeitsvorteil für indizierte Suchvorgänge und das Auspacken:

$ 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

Hier So wird der Tuple (10, 20) gespeichert:

    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;

Hier So wird die Liste [10, 20] gespeichert:

    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;

Beachten Sie, dass das Tuple-Objekt die beiden Datenzeiger direkt enthält, während das Listenobjekt über eine zusätzliche Ebene der Indirektion zu einem externen Array verfügt, das die beiden Datenzeiger enthält.

9

Im Wesentlichen, weil Tuples Unveränderbarkeit bedeutet, dass der Interpreter im Vergleich zur Liste eine schlankere, schnellere Datenstruktur verwenden kann.

5
Dan Breslau

Ein Bereich, in dem eine Liste deutlich schneller ist, ist die Konstruktion aus einem Generator. Insbesondere sind Listenverständnisse viel schneller als das nächstliegende Tuple-Äquivalent Tuple() mit einem Generatorargument:

$ python --version
Python 3.6.0rc2
$ python -m timeit 'Tuple(x * 2 for x in range(10))'
1000000 loops, best of 3: 1.34 usec per loop
$ python -m timeit 'list(x * 2 for x in range(10))'
1000000 loops, best of 3: 1.41 usec per loop
$ python -m timeit '[x * 2 for x in range(10)]'
1000000 loops, best of 3: 0.864 usec per loop

Beachten Sie insbesondere, dass Tuple(generator) ein kleines bisschen schneller zu sein scheint als list(generator), aber [elem for elem in generator] ist viel schneller als beide.

1
Dan Passaro

Tupel werden vom Python-Compiler als eine unveränderliche Konstante Identifiziert. Der Compiler hat also nur einen Eintrag in der Hash-Tabelle erstellt und nie geändert

Listen sind veränderliche Objekte. Der Compiler aktualisiert also den Eintrag, wenn wir die Liste Aktualisieren, sodass er im Vergleich zu Tuple etwas langsamer ist

0
y durga prasad