2013-04-05 3 views
8

J'ai plusieurs processus traitant chacun des listes qui ont 40000 tuples. cela presque maxes la mémoire disponible sur la machine. si je fais ceci:Efficacité python et grands objets en mémoire

 while len(collection) > 0: 
      row = collection.pop(0) 
      row_count = row_count + 1 
      new_row = [] 
      for value in row: 
       if value is not None: 
        in_chars = str(value) 
       else: 
        in_chars = "" 

       #escape any naughty characters 
       new_row.append("".join(["\\" + c if c in redshift_escape_chars else c for c in in_chars])) 
      new_row = "\t".join(new_row) 
      rows += "\n"+new_row 
      if row_count % 5000 == 0: 
       gc.collect() 

est-ce que cela libère plus de mémoire?

+1

Je ne veux pas être sarcastique mais pourquoi ne pas simplement l'essayer et voir? –

+0

pensé que quelqu'un pourrait savoir si cela en vaut la peine avec les frais généraux du GC ou des trucs internes python en cours que j'aurais oublié. – tipu

+2

Si cela est possible, il pourrait être utile d'examiner la possibilité d'utiliser des itérateurs et de traiter tous les tuples de 40 k à la fois, en construisant la liste et en les traitant en même temps. Cela ajoutera de la complexité et ne vaudra peut-être pas la peine. – Moshe

Répondre

7

Comme le collection diminue au même rythme que rows, votre consommation de mémoire reste stable.L'appel gc.collect() ne va pas faire beaucoup de différence.

La gestion de la mémoire dans CPython est subtile. Le fait de supprimer des références et d'exécuter un cycle de collecte ne signifie pas nécessairement que la mémoire sera renvoyée au système d'exploitation. Voir this answer for details. Pour vraiment économiser de la mémoire, vous devez structurer ce code autour des générateurs et des itérateurs au lieu de grandes listes d'éléments. Je suis très surpris que vous ayez des délais d'attente de connexion car la récupération de toutes les lignes ne devrait pas prendre beaucoup plus de temps que d'aller chercher une ligne à la fois et d'effectuer le traitement simple que vous faites. Peut-être devrions-nous jeter un oeil à votre code db-fetching?

Si le traitement de ligne à la fois n'est vraiment pas une possibilité, gardez au moins vos données comme une variable non modifiable et effectuez tous les traitements avec des générateurs et des itérateurs.

Je vais décrire ces différentes approches.

D'abord, certaines fonctions communes:

# if you don't need random-access to elements in a sequence 
# a deque uses less memory and has faster appends and deletes 
# from both the front and the back. 
from collections import deque 
from itertools import izip, repeat, islice, chain 
import re 

re_redshift_chars = re.compile(r'[abcdefg]') 

def istrjoin(sep, seq): 
    """Return a generator that acts like sep.join(seq), but lazily 

    The separator will be yielded separately 
    """ 
    return islice(chain.from_iterable(izip(repeat(sep), seq)), 1, None) 

def escape_redshift(s): 
    return re_redshift_chars.sub(r'\\\g<0>', s) 

def tabulate(row): 
    return "\t".join(escape_redshift(str(v)) if v is not None else '' for v in row) 

Maintenant, l'idéal est rangée à-un-temps de traitement, comme ceci:

cursor = db.cursor() 
cursor.execute("""SELECT * FROM bigtable""") 
rowstrings = (tabulate(row) for row in cursor.fetchall()) 
lines = istrjoin("\n", rowstrings) 
file_like_obj.writelines(lines) 
cursor.close() 

Cela prendra le moins possible mémoire - seulement une ligne à la fois.

Si vous avez vraiment besoin de stocker l'ensemble de résultats, vous pouvez modifier le code légèrement:

cursor = db.cursor() 
cursor.execute("SELECT * FROM bigtable") 
collection = deque(cursor.fetchall()) 
cursor.close() 
rowstrings = (tabulate(row) for row in collection) 
lines = istrjoin("\n", rowstrings) 
file_like_obj.writelines(lines) 

Maintenant, nous nous réunissons tous les résultats dans collection premier qui reste entièrement en mémoire pour l'ensemble de l'exécution du programme. Cependant, nous pouvons également dupliquer votre approche de suppression des éléments de collection tels qu'ils sont utilisés. Nous pouvons garder la même "forme de code" en créant un générateur que vide sa collection source comme cela fonctionne. Il ressemblerait à quelque chose comme ceci:

def drain(coll): 
    """Return an iterable that deletes items from coll as it yields them. 

    coll must support `coll.pop(0)` or `del coll[0]`. A deque is recommended! 
    """ 
    if hasattr(coll, 'pop'): 
     def pop(coll): 
      try: 
       return coll.pop(0) 
      except IndexError: 
       raise StopIteration 
    else: 
     def pop(coll): 
      try: 
       item = coll[0] 
      except IndexError: 
       raise StopIteration 
      del coll[0] 
      return item 
    while True: 
     yield pop(coll) 

Maintenant, vous pouvez facilement remplacer drain(collection) pour collection lorsque vous souhaitez libérer de la mémoire que vous allez. Après drain(collection) est épuisé, l'objet collection sera vide.

2

Si votre algorithme dépend de pop'ing à partir du côté gauche ou au début d'une liste, vous pouvez utiliser l'objet deque de collections comme alternative plus rapide.

A titre de comparaison:

import timeit 

f1=''' 
q=deque() 
for i in range(40000): 
    q.append((i,i,'tuple {}'.format(i))) 

while q: 
    q.popleft() 
''' 

f2=''' 
l=[] 
for i in range(40000): 
    l.append((i,i,'tuple {}'.format(i))) 

while l: 
    l.pop(0) 
''' 

print 'deque took {:.2f} seconds to popleft()'.format(timeit.timeit(stmt=f1, setup='from collections import deque',number=100)) 
print 'list took {:.2f} seconds to pop(0)'.format(timeit.timeit(stmt=f2,number=100)) 

Prints:

deque took 3.46 seconds to to popleft() 
list took 37.37 seconds to pop(0) 

Donc, pour ce test particulier de sauter depuis le début de la liste ou la file d'attente, deque est plus de 10 fois plus rapide.

Ce grand avantage n'est cependant que pour le côté gauche. Si vous exécutez ce même test avec pop() sur les deux la vitesse est à peu près la même. Vous pouvez également inverser la liste en place et pop à partir du côté droit pour obtenir les mêmes résultats que popleft de la deque. En termes d '«efficacité», il sera beaucoup plus efficace de traiter des lignes uniques à partir de la base de données. Si ce n'est pas une option, traitez votre liste (ou deque) 'collection' en place.

Essayez quelque chose dans ce sens.

Tout d'abord, sortir le traitement de la ligne:

def process_row(row): 
    # I did not test this obviously, but I think I xlated your row processing faithfully 
    new_row = [] 
    for value in row: 
     if value: 
      in_chars = str(value)   
     else 
      in_char='' 
     new_row.append("".join(["\\" + c if c in redshift_escape_chars else c for c in in_chars])) 
    return '\t'.join(new_row)  

regarder maintenant à utiliser un deque pour permettre pops rapide de la gauche:

def cgen(collection): 
    # if collection is a deque: 
    while collection: 
     yield '\n'+process_row(collection.popleft()) 

Ou si vous voulez coller à une liste:

def cgen(collection): 
    collection.reverse() 
    while collection: 
     yield '\n'+process_row(collection.pop()) 

Je pense que votre approche originale de la pop (0), le processus de la ligne et appeler tous les gc 5000 lignes est pr probablement sous-optimal. Le gc sera appelé automatiquement beaucoup plus souvent que cela de toute façon.

Ma dernière recommandation:

  1. Utilisez un deque. Il ressemble à un list mais plus rapide pour le côté gauche ou les pops;
  2. Utilisez popleft() afin de ne pas avoir à inverser la liste (si l'ordre de collection est significatif);
  3. Traitez votre collection en place en tant que générateur;
  4. Jeter la notion d'appeler gc car il ne fait rien pour vous.
  5. Jetez 1-4 ici si vous pouvez simplement appeler le db et obtenir 1 ligne et traiter 1 ligne à la fois!
+2

Puis-je connaître les pensées de l'électeur? – dawg