web-dev-qa-db-fra.com

Efficacité de l'utilisation d'une liste Python comme file d'attente

Un collègue a récemment écrit un programme dans lequel il a utilisé une liste Python comme file d'attente. En d'autres termes, il a utilisé .append(x) pour insérer des éléments et .pop(0) pour supprimer des éléments.

Je sais que Python a collections.deque et j'essaie de savoir s'il faut passer mon temps (limité) pour réécrire ce code pour l'utiliser. En supposant que nous effectuons des millions d'ajouts et de pops mais que nous n'avons jamais plus de quelques milliers d'entrées, son utilisation de la liste sera-t-elle un problème?

En particulier, le tableau sous-jacent utilisé par l'implémentation de la liste Python continuera-t-il de croître indéfiniment avec des millions de spots même si la liste ne contient que mille choses, ou Python finira-t-il par faire un realloc et libérer une partie de cette mémoire?

43
Eli Courtwright

Vous ne manquerez pas de mémoire en utilisant l'implémentation list, mais les performances seront médiocres. De les docs :

Bien que les objets list prennent en charge des opérations similaires, ils sont optimisés pour des opérations rapides de longueur fixe et entraînent O(n) coûts de déplacement de la mémoire pour pop(0) et insert(0, v) opérations qui modifient à la fois la taille et la position de la représentation des données sous-jacentes.

Donc, utiliser un deque sera beaucoup plus rapide.

32
John Millikin

Certaines réponses revendiquaient un avantage de vitesse "10x" pour deque par rapport à la liste utilisée comme FIFO lorsque les deux ont 1000 entrées, mais c'est un peu une surenchère:

$ python -mtimeit -s'q=range(1000)' 'q.append(23); q.pop(0)'
1000000 loops, best of 3: 1.24 usec per loop
$ python -mtimeit -s'import collections; q=collections.deque(range(1000))' 'q.append(23); q.popleft()'
1000000 loops, best of 3: 0.573 usec per loop

python -mtimeit est votre ami - une approche de micro-benchmarking vraiment utile et simple! Avec cela, vous pouvez bien sûr également explorer de manière triviale les performances dans des cas beaucoup plus petits:

$ python -mtimeit -s'q=range(100)' 'q.append(23); q.pop(0)'
1000000 loops, best of 3: 0.972 usec per loop
$ python -mtimeit -s'import collections; q=collections.deque(range(100))' 'q.append(23); q.popleft()'
1000000 loops, best of 3: 0.576 usec per loop

(pas très différent pour 12 au lieu de 100 articles btw), et pour les plus gros:

$ python -mtimeit -s'q=range(10000)' 'q.append(23); q.pop(0)'
100000 loops, best of 3: 5.81 usec per loop
$ python -mtimeit -s'import collections; q=collections.deque(range(10000))' 'q.append(23); q.popleft()'
1000000 loops, best of 3: 0.574 usec per loop

Vous pouvez voir que la revendication de O(1) performances pour deque est bien fondée, alors qu'une liste est plus de deux fois plus lente autour de 1000 articles, un ordre de grandeur d'environ 10 000. Vous pouvez également voir que même dans de tels cas, vous ne perdez que 5 microsecondes environ par paire append/pop et décidez de l'importance de ce gaspillage (bien que si c'est tout ce que vous faites avec ce conteneur, deque n'ait aucun inconvénient, vous pourriez donc aussi bien changer même si 5 usec plus ou moins ne feront pas une différence importante).

71
Alex Martelli

D'après Beazley's ( Python Essential Reference, Fourth Edition , p. 194:

Certains modules de bibliothèque fournissent de nouveaux types qui surpassent les fonctions intégrées dans certaines tâches. Par exemple, le type collections.deque fournit des fonctionnalités similaires à une liste mais a été hautement optimisé pour l'insertion d'éléments aux deux extrémités. Une liste, en revanche, n'est efficace que lors de l'ajout d'éléments à la fin. Si vous insérez des éléments à l'avant, tous les autres éléments doivent être déplacés afin de faire de la place. Le temps nécessaire pour ce faire s'allonge à mesure que la liste s'allonge. Juste pour vous donner une idée de la différence, voici une mesure temporelle de l'insertion d'un million d'articles au début d'une liste et d'un déque:

Et voici cet exemple de code:

>>> from timeit import timeit
>>> timeit('s.appendleft(37)', 'import collections; s = collections.deque()', number=1000000)
0.13162776274638258
>>> timeit('s.insert(0,37)', 's = []', number=1000000)
932.07849908298408

Les horaires proviennent de ma machine.


Mise à jour 2012-07-01

>>> from timeit import timeit
>>> n = 1024 * 1024
>>> while n > 1:
...     print '-' * 30, n
...     timeit('s.appendleft(37)', 'import collections; s = collections.deque()', number=n)
...     timeit('s.insert(0,37)', 's = []', number=n)
...     n >>= 1
... 
------------------------------ 1048576
0.1239769458770752
799.2552740573883
------------------------------ 524288
0.06924104690551758
148.9747350215912
------------------------------ 262144
0.029170989990234375
35.077512979507446
------------------------------ 131072
0.013737916946411133
9.134140014648438
------------------------------ 65536
0.006711006164550781
1.8818109035491943
------------------------------ 32768
0.00327301025390625
0.48307204246520996
------------------------------ 16384
0.0016388893127441406
0.11021995544433594
------------------------------ 8192
0.0008249282836914062
0.028419017791748047
------------------------------ 4096
0.00044918060302734375
0.00740504264831543
------------------------------ 2048
0.00021195411682128906
0.0021741390228271484
------------------------------ 1024
0.00011205673217773438
0.0006101131439208984
------------------------------ 512
6.198883056640625e-05
0.00021386146545410156
------------------------------ 256
2.9087066650390625e-05
8.797645568847656e-05
------------------------------ 128
1.5974044799804688e-05
3.600120544433594e-05
------------------------------ 64
8.821487426757812e-06
1.9073486328125e-05
------------------------------ 32
5.0067901611328125e-06
1.0013580322265625e-05
------------------------------ 16
3.0994415283203125e-06
5.9604644775390625e-06
------------------------------ 8
3.0994415283203125e-06
5.0067901611328125e-06
------------------------------ 4
3.0994415283203125e-06
4.0531158447265625e-06
------------------------------ 2
2.1457672119140625e-06
2.86102294921875e-06
16
hughdbrown

Chaque .pop(0) prend N étapes, car la liste doit être réorganisée. La mémoire requise ne se développera pas sans fin et ne sera aussi grande que celle requise pour les éléments qui sont conservés.

Je recommanderais d'utiliser deque pour obtenir O(1) ajouter et sauter de l'avant.

4
bayer

il semble qu'un peu de tests empiriques pourrait être la meilleure chose à faire ici - les problèmes de second ordre pourraient améliorer une approche dans la pratique, même si elle n'est pas meilleure en théorie.

2
Peter