Existe-t-il une différence de performance entre les tuples et les listes en ce qui concerne l'instanciation et la récupération d'éléments?
Le module dis
désassemble le code d'octet d'une fonction et permet de voir la différence entre les tuples et les listes.
Dans ce cas, vous pouvez voir que l'accès à un élément génère un code identique, mais que l'attribution d'un tuple est beaucoup plus rapide que l'attribution d'une liste.
>>> 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
En général, vous pouvez vous attendre à ce que les n-uplets soient légèrement plus rapides. Cependant, vous devez absolument tester votre cas spécifique (si la différence peut avoir une incidence sur les performances de votre programme - souvenez-vous que "l'optimisation prématurée est la racine de tous les maux").
Python rend cela très facile: timeit est votre ami.
$ 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
et...
$ 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
Ainsi, dans ce cas, l'instanciation est presque d'un ordre de grandeur plus rapide pour le tuple, mais l'accès aux éléments est en réalité un peu plus rapide pour la liste! Donc, si vous créez quelques nuplets et y accédez plusieurs fois, il peut être plus rapide d’utiliser des listes à la place.
Bien sûr, si vous voulez changer un élément, la liste sera certainement plus rapide car vous devrez créer un nouveau Tuple entier pour changer un élément de il (puisque les tuples sont immuables).
Les tuples ont tendance à avoir de meilleurs résultats que les listes dans presque toutes les catégories:
1) Les tuples peuvent être plié 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.
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
L'exécution de Tuple(some_Tuple)
retourne immédiatement. 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)
nécessite que toutes les données soient copiées dans une nouvelle liste:
>>> a = [10, 20, 30]
>>> b = list(a)
>>> a is b
False
É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 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 Voici comment 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 Voici comment la liste [10, 20]
Est stockée:
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.
Les tuples, étant immuables, utilisent plus efficacement la mémoire; listes, pour plus d'efficacité, surpasse la mémoire afin de permettre les ajouts sans constante realloc
s. Donc, si vous voulez parcourir une séquence constante de valeurs dans votre code (par exemple, for direction in 'up', 'right', 'down', 'left':
), Les n-uplets sont préférés, car ils sont précalculés au moment de la compilation.
Les vitesses d'accès doivent être les mêmes (elles sont toutes deux stockées sous forme de tableaux contigus dans la mémoire).
Mais alist.append(item)
est bien préférable à atuple+= (item,)
lorsque vous traitez avec des données modifiables. Rappelez-vous que les n-uplets sont destinés à être traités comme des enregistrements sans noms de champs.
Vous devriez également considérer le module array
de la bibliothèque standard si tous les éléments de votre liste ou de votre nuplet sont du même type C. Cela prendra moins de mémoire et peut être plus rapide.
Les tuples devraient être légèrement plus efficaces et, à cause de cela, plus rapidement que les listes, car elles sont immuables.
Voici un autre petit repère, juste pour le plaisir ..
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)
Faisons la moyenne de ceux-ci:
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
Vous pouvez appeler cela presque non concluant.
Mais bien sûr, tuples prirent 101.239%
l'heure, ou 1.239%
temps supplémentaire pour effectuer le travail par rapport aux listes.