web-dev-qa-db-fra.com

Pourquoi Tuple est-il plus rapide que la liste en Python?

Je viens de lire dans "Plongez dans Python" que "les n-uplets sont plus rapides que les listes".

Tuple est immuable, et la liste est modifiable, mais je ne comprends pas très bien pourquoi Tuple est plus rapide.

Quelqu'un a fait un test de performance à ce sujet?

53
Vimvq1987

Le rapport "vitesse de construction" indiqué est valable uniquement pour constant tuples (ceux dont les éléments sont exprimés en littéraux). Observez attentivement (et répétez sur votre machine - il vous suffit de taper les commandes dans une fenêtre de commande/shell!) ...:

$ 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

Je n’ai pas fait les mesures sur la version 3.0 parce que, bien sûr, je ne l’ai pas autour de moi. C’est totalement obsolète et il n’ya absolument aucune raison de la garder, car la version 3.1 est supérieure à tout point de vue (Python 2.7, si vous peut effectuer une mise à niveau, les mesures étant presque 20% plus rapides que la version 2.6 dans chaque tâche - et la version 2.6, comme vous le voyez, est plus rapide que la version 3.1 - donc, si vous tenez vraiment à la performance, Python 2.7 est vraiment la seule version à vous proposer. aller pour!).

Quoi qu'il en soit, le point clé ici est que, dans chaque version de Python, créer une liste à partir de littéraux constants est à peu près identique, ou légèrement plus lent, que de la construire à partir de valeurs référencées par des variables; mais les tuples se comportent très différemment - construire un tuple à partir de littéraux constants est généralement trois fois plus rapide que de le construire à partir de valeurs référencées par des variables! Vous pouvez vous demander comment cela peut être, non? -)

Réponse: un tuple constitué de littéraux constants peut facilement être identifié par le compilateur Python comme étant un littéral constant immuable lui-même: il est donc construit une seule fois lorsque le compilateur convertit la source en bytecodes et qu'il est caché dans la "table des constantes" "de la fonction ou du module concerné. Quand ces bytecodes s'exécutent, il leur suffit de récupérer la constante pré-construite Tuple - hé hop! -)

Cette optimisation simple ne peut pas être appliquée aux listes, car une liste est un objet mutable, il est donc crucial que, si la même expression telle que [1, 2, 3] s'exécute deux fois (dans une boucle - le module timeit effectue la boucle pour vous ;-), un nouvel objet de liste est construit chaque fois à nouveau - et cette construction (comme la construction d'un Tuple lorsque le compilateur ne peut pas l'identifier de manière triviale comme un objet constant et immuable à la compilation) prend un peu de temps.

Cela étant dit, la construction de Tuple (lorsque les deux constructions doivent réellement se produire) est toujours environ deux fois plus rapide que la construction de liste - et la différence {que peut être expliquée par la simplicité même de Tuple, à laquelle d'autres réponses ont mentionné à plusieurs reprises. Mais, cette simplicité ne représente pas une accélération de six fois ou plus, comme vous le constatez si vous ne comparez que la construction de listes et de tuples avec de simples littéraux constants comme éléments! _)

80
Alex Martelli

Grâce à la puissance du module timeit, vous pouvez souvent résoudre vous-même les questions relatives aux performances:

$ 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

Cela montre que Tuple est négligeable plus rapidement que la liste pour l'itération. J'obtiens des résultats similaires pour l'indexation, mais pour la construction, Tuple détruit la 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

Donc, si la vitesse d'itération ou l'indexation sont les seuls facteurs, il n'y a effectivement aucune différence, mais pour la construction, les n-uplets gagnent.

16
Alec Thomas

Alex a donné une excellente réponse, mais je vais essayer de développer quelques points qui méritent d’être mentionnés. Les différences de performances sont généralement faibles et spécifiques à la mise en œuvre: ne pariez donc pas la ferme dessus.

Dans CPython, les n-uplets étant stockés dans un seul bloc de mémoire, la création d'un nouveau n-uplet implique au pire un seul appel pour allouer de la mémoire. Les listes sont allouées en deux blocs: le fixe avec toutes les informations sur l’objet Python et un bloc de taille variable pour les données. C'est en partie pourquoi la création d'un tuple est plus rapide, mais cela explique probablement aussi la légère différence de vitesse d'indexation car il y a un pointeur de moins à suivre.

Il existe également des optimisations dans CPython pour réduire les allocations de mémoire: les objets de liste désaffectés sont enregistrés sur une liste libre afin d’être réutilisés, mais l’allocation d’une liste non vide nécessite toujours une allocation de mémoire pour les données. Les nuplets sont sauvegardés sur 20 listes libres pour des nuplets de tailles différentes. L'attribution d'un petit nuplet n'aura souvent pas besoin d'appels d'allocation de mémoire.

De telles optimisations sont utiles dans la pratique, mais elles peuvent également rendre risqué de trop dépendre des résultats de 'timeit' et sont bien sûr complètement différentes si vous passez à quelque chose comme IronPython où l'allocation de mémoire fonctionne très différemment.

16
Duncan

Résumé

Les tuples ont tendance à avoir de meilleurs résultats que les listes dans presque toutes les catégories:

1) Les tuples peuvent être pliés en permanence .

2) Les tuples peuvent être réutilisés au lieu d'être copiés.

3) Les n-uplets sont compacts et ne surallouent pas.

4) Les tuples référencent directement leurs éléments.

Les tuples peuvent être constamment pliés

Des nuplets de constantes peuvent être pré-calculés par l'optimiseur de judas ou l'optimiseur AST de Python. Les listes, par contre, se construisent à partir de zéro:

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

Vous n'avez pas besoin de copier les tuples

Lancer Tuple(some_Tuple) retourne immédiatement lui-même. Les n-uplets étant immuables, il n’est pas nécessaire de les copier:

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

En revanche, list(some_list) exige que toutes les données soient copiées dans une nouvelle liste:

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

Tuples ne pas trop allouer

Étant donné que la taille d'un tuple est fixe, il peut être stocké de manière plus compacte que les listes qui doivent être surallouées pour rendre les opérations append () efficaces.

Cela donne aux tuples un avantage d'espace Nice:

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

Voici le commentaire de Objects/listobject.c qui explique ce que font les listes:

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

Les tuples se réfèrent directement à leurs éléments

Les références aux objets sont incorporées directement dans un objet Tuple. En revanche, les listes ont une couche supplémentaire d'indirection vers un tableau externe de pointeurs.

Cela donne aux tuples un petit avantage en termes de vitesse pour les recherches indexées et le décompactage:

$ 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

Ici est la façon dont le Tuple (10, 20) est stocké:

    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;

Ici est le mode de stockage de la liste [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;

Notez que l'objet Tuple incorpore directement les deux pointeurs de données, tandis que l'objet liste comporte une couche d'indirection supplémentaire vers un tableau externe contenant les deux pointeurs de données.

9
Raymond Hettinger

Essentiellement parce que l’immuabilité de Tuple signifie que l’interprète peut utiliser une structure de données plus légère et plus rapide, comparée à la liste.

5
Dan Breslau

Un domaine où une liste est nettement plus rapide est la construction à partir d'un générateur, et en particulier, les compréhensions de liste sont beaucoup plus rapides que l'équivalent Tuple le plus proche, Tuple() avec un argument de générateur:

$ 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

Notez en particulier que Tuple(generator) semble être un peu plus rapide que list(generator), mais [elem for elem in generator] est beaucoup plus rapide que les deux.

1
Dan Passaro

Les compilations sont identifiées par le compilateur python comme une constante immuable. Le compilateur a créé une seule entrée dans la table de hachage et n’a jamais changé.

Les listes sont des objets modifiables. Le compilateur met à jour l'entrée lorsque nous mettons à jour la listeSo il est un peu plus lent comparé à Tuple

0
y durga prasad