58 votes

Est-il un moyen de contourner une liste Python.append() devient progressivement plus lente dans une boucle, car la liste pousse?

J'ai un gros fichier que je suis en train de lire, et de convertir tous les quelques lignes à une instance d'un Objet.

Depuis que je suis une boucle dans le fichier, je planque l'exemple d'une liste à l'aide de la liste.append(exemple), puis continuer la boucle.

C'est un fichier qui est autour de ~100 MO donc il n'est pas trop grande, mais aussi la liste grandit, la boucle se ralentit progressivement. (J'ai l'impression que le temps de chaque tour dans la boucle).

Ce n'est pas intrinsèque à la boucle ~ lorsque j'imprime chaque nouvelle instance que je boucle dans le fichier, le programme progresse à vitesse constante ~ c'est seulement quand je les ajoute à une liste, il est au ralenti.

Mon ami m'a suggéré de désactiver la collecte des ordures avant la boucle while et en lui permettant par la suite & de faire une collecte des ordures appel.

Quelqu'un d'autre s'observer un problème similaire avec la liste.ajouter devient plus lent? Est-il un autre moyen de contourner cela?


Je vais essayer les deux choses suggérées ci-dessous.

(1) "pré-affectation" de la mémoire ~ quelle est la meilleure façon de le faire? (2) Essayez d'utiliser deque

Plusieurs postes (voir le commentaire de Alex Martelli) a suggéré une fragmentation de la mémoire (il a une grande quantité de mémoire disponible comme je le fais) ~ mais pas évident de correctifs pour le rendement de ce.

À reproduire le phénomène, s'il vous plaît exécuter le test de code ci-dessous dans les réponses, et supposons que les listes des données utiles.


gc.disable() et gc.enable() permet de timing. Je vais aussi faire une analyse minutieuse de l'endroit où tous les temps est passé.

99voto

Erik Garrison Points 503

La mauvaise performance que vous observez est causé par un bogue dans le Python garbage collector. Pour résoudre ce problème, désactivez la collecte des ordures que vous construisez la liste et de l'activer lorsque vous avez terminé. Vous trouverez un rendement qui se rapproche de la amoritized 0(1) le comportement attendu de la liste en ajoutant en Python.

(Vous pouvez également ajuster le garbage collector de déclencheurs ou de manière sélective virés comme vous le progrès, mais je ne suis pas d'explorer ces options dans cette réponse, parce qu'ils sont plus complexes, et je soupçonne que votre cas est favorable à la solution ci-dessus.)

Arrière-plan:

Voir: http://bugs.python.org/issue4074 et aussi http://docs.python.org/release/2.5.2/lib/module-gc.html

Le rapporteur observe que, ajoutant des objets complexes, des objets qui ne sont pas des nombres ou des chaînes de caractères) à une liste ralentit de manière linéaire avec la liste se développe dans la longueur.

La raison de ce comportement est que le garbage collector est à vérifier et revérifier chaque objet de la liste pour voir si elles sont admissibles pour la collecte des ordures. Ce comportement provoque l'augmentation linéaire dans le temps pour ajouter des objets à une liste. Un correctif devrait débarquer en py3k, de sorte qu'il ne devrait pas s'appliquer à l'interprète que vous utilisez.

Test:

J'ai couru un test pour le démontrer. Pour 1k itérations je ajouter 10k objets d'une liste, et d'enregistrer le moteur d'exécution pour chaque itération. L'ensemble de l'exécution différence est immédiatement évident. Avec la collecte des ordures désactivé au cours de la boucle interne de l'essai, le moteur d'exécution sur mon système est de 18,6 s. Avec la collecte des ordures activé pour l'ensemble du test, la durée d'exécution est 899.4 s.

C'est le test:

import time
import gc

class A:
    def __init__(self):
        self.x = 1
        self.y = 2
        self.why = 'no reason'

def time_to_append(size, append_list, item_gen):
    t0 = time.time()
    for i in xrange(0, size):
        append_list.append(item_gen())
    return time.time() - t0

def test():
    x = []
    count = 10000
    for i in xrange(0,1000):
        print len(x), time_to_append(count, x, lambda: A())

def test_nogc():
    x = []
    count = 10000
    for i in xrange(0,1000):
        gc.disable()
        print len(x), time_to_append(count, x, lambda: A())
        gc.enable()

Source: http://hypervolu.me/~erik/programming/python_lists/listtest.py.txt

Résultat graphique: le Rouge est de la cg sur, le bleu est la cg off. l'axe y est la seconde échelle logarithmique.

Comme pour les deux parcelles diffèrent de plusieurs ordres de grandeur dans la composante y, ici, ils sont de façon indépendante avec l'axe des y ont évolué de manière linéaire.

Il est intéressant de noter, avec la collecte des ordures, nous ne voyons que de petits pics en exécution par 10k ajoute, ce qui suggère que Python liste réaffectation des coûts relativement faibles. En tout cas, ils sont de plusieurs ordres de grandeur inférieure à la collecte des ordures coûts.

La densité des parcelles ci-dessus il est difficile de voir qu'avec le garbage collector, la plupart des intervalles réellement avoir de bonnes performances; c'est seulement lorsque le garbage collector cycles que nous rencontrons le comportement pathologique. Vous pouvez observer ce dans cet histogramme de 10k ajouter de temps. La plupart des points de données à l'automne autour de 0,02 s par 10k ajoute.

Les données brutes utilisées pour la production de ces parcelles peut être trouvé à http://hypervolu.me/~erik/programmation/python_lists/

13voto

Mike Graham Points 22480

Il n'y a rien à contourner: l' ajoutant à une liste est O(1) amorti.

Une liste (Disponible) est un tableau au moins aussi longue que la liste et jusqu'à deux fois plus longtemps. Si le tableau n'est pas complet, en ajoutant à une liste est tout aussi simple que l'affectation d'un des membres du groupe (O(1)). Chaque fois que le tableau est plein, il est automatiquement doublé de taille. Cela signifie, qu'à l'occasion d'un O(n) l'opération est nécessaire, mais il est seulement exigé que chaque n opérations, et il est de plus en plus rarement que la liste devient grand. O(n) / n ==> O(1). (Dans d'autres implémentations, les noms et les détails pourraient potentiellement changer, mais en même temps les propriétés sont tenus d'être maintenue.)

Ajoutant à une liste déjà échelles.

Est-il possible que lorsque le fichier est gros vous n'êtes pas en mesure de mettre tout le monde dans la mémoire, et vous êtes confrontés à des problèmes avec le système d'exploitation pagination sur disque? Est-il possible, c'est une autre partie de votre algorithme qui n'évolue pas bien?

6voto

FogleBird Points 23405

Beaucoup de ces réponses ne sont que des suppositions sauvages. J'aime Mike Graham est le meilleur parce qu'il est juste à propos de la façon dont les listes sont mises en œuvre. Mais j'ai écrit un code pour reproduire votre demande et de regarder plus loin. Voici quelques conclusions.

Voici ce que j'ai commencé.

import time
x = []
for i in range(100):
    start = time.clock()
    for j in range(100000):
        x.append([])
    end = time.clock()
    print end - start

Je suis juste en ajoutant vide listes la liste x. J'ai l'impression d'une durée pour chaque tranche de 100 000 ajoute, 100 fois. Il ralentit comme vous l'avez réclamé. (0.03 secondes pour la première itération, et 0,84 secondes pour la dernière... toute une différence.)

Évidemment, si vous instanciez une liste, mais ne pas ajouter à x, il s'exécute plus rapidement et n'évoluent pas au cours du temps.

Mais si vous changez x.append([]) de x.append('hello world'), il n'y a pas d'augmentation de la vitesse à tous. Le même objet est de s'ajouter à la liste de 100 * 100 000 fois.

Ce que je fais de cette:

  • La diminution de la vitesse n'a rien à voir avec la taille de la liste. Il a à voir avec le nombre de vivre des objets Python.
  • Si vous n'avez pas à ajouter des éléments à la liste, ils sont juste des ordures collectées tout de suite et ne sont plus gérés par Python.
  • Si vous ajoutez la même chose et plus, le nombre de vivre des objets Python n'est pas en augmentation. Mais la liste n'ont pour redimensionner lui-même à chaque fois dans un tout. Mais ce n'est pas la source du problème de performances.
  • Puisque vous êtes à la création et l'ajout de beaucoup d'objets nouvellement créés à une liste, ils restent vivants et ne sont pas des ordures collectées. Le ralentissement a probablement quelque chose à voir avec cela.

Aussi loin que les internes de Python qui pourraient expliquer cela, je ne suis pas sûr. Mais je suis sûr que la liste de la structure de données n'est pas le coupable.

2voto

Tomasz Zielinski Points 9300

Pouvez-vous essayer http://docs.python.org/release/2.5.2/lib/deque-objects.html l'allocation est prévu que le nombre d'éléments requis dans votre liste? ? Je parie que la liste est une zone contiguë de stockage qui doit être réalloués et copié tous les quelques itérations. (similaire à certains populaire implémentations de std::vector en c++)

EDIT: Soutenu par http://www.python.org/doc/faq/general/#how-are-lists-implemented

1voto

Nathan Labenz Points 400

J'ai rencontré ce problème lors de l'utilisation des tableaux Numpy, créé comme suit:

import numpy
theArray = array([],dtype='int32')

L'ajout de ce tableau dans une boucle a pris progressivement plus longtemps que la matrice a grandi, qui a été un deal-breaker étant donné que j'ai eu 14M ajoute à faire.

Le garbage collector solution décrite ci-dessus semblait prometteuse, mais n'a pas fonctionné.

Ce n'travail a été la création du tableau avec une taille définie comme suit:

theArray = array(arange(limit),dtype='int32')

Assurez-vous juste que la limite est plus grand que le tableau dont vous avez besoin.

Vous pouvez ensuite définir chaque élément de la matrice directement:

theArray[i] = val_i

Et à la fin, si nécessaire, vous pouvez enlever la partie inutilisée de la matrice

theArray = theArray[:i]

Cela fait une ÉNORME différence dans mon cas.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X