2013-04-05 8 views
8

Mam wiele procesów, z których każdy ma do czynienia z listami, które mają 40000 krotek. to prawie maksymalizuje pamięć dostępną na komputerze. jeśli to zrobię:wydajność Pythona i duże obiekty w pamięci

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

czy to za darmo więcej pamięci?

+1

Nie chcę być chytrym, ale czemu nie spróbujesz tego i nie zobaczysz? –

+0

Wyobraźcie sobie, że ktoś może wiedzieć, czy to jest ogólnie warte tego z napowietrznym GC, czy też z niektórych wewnętrznych rzeczy Pythona, które przeoczyłbym. – tipu

+2

Jeśli jest to możliwe, warto rozważyć możliwość korzystania z iteratorów i zamiast przetwarzania wszystkich krotek 40k jednocześnie, budując listę i przetwarzając je w tym samym czasie. To będzie trochę większa złożoność i może nie być warte wysiłku. – Moshe

Odpowiedz

7

Ponieważ wskaźnik collection kurczy się z tą samą szybkością, co wzrost liczby rows, zużycie pamięci pozostanie stabilne.Wywołanie gc.collect() nie ma większego znaczenia.

Zarządzanie pamięcią w CPython jest subtelne. Tylko usunięcie odniesień i uruchomienie cyklu zbierania niekoniecznie oznacza, że ​​pamięć zostanie zwrócona do systemu operacyjnego. Zobacz this answer for details.

Aby naprawdę zaoszczędzić pamięć, należy ustrukturyzować ten kod wokół generatorów i iteratorów zamiast dużych list elementów. Jestem bardzo zaskoczony, że mówisz, że masz limity czasu połączenia, ponieważ pobranie wszystkich wierszy nie powinno zająć dużo więcej czasu niż pobranie wiersza za jednym razem i wykonanie prostego przetwarzania, które robisz. Być może powinniśmy rzucić okiem na twój kod pobierania DB?

Jeśli przetwarzanie wiersza nie jest możliwe, to przynajmniej zachowaj dane jako niezmienną deque i wykonaj wszystkie przetwarzanie na nim za pomocą generatorów i iteratorów.

Opiszę te różne podejścia.

Przede wszystkim pewne wspólne funkcje:

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

Teraz ideałem jest rząd-at-a-czas przetwarzania, tak:

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

To zajmie najmniejszą możliwą ilość pamięć - tylko rząd naraz.

Jeśli naprawdę trzeba przechowywać całą resultset, można nieco zmodyfikować kod:

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) 

Teraz musimy zebrać wszystkie wyniki na collection pierwszym, który pozostaje w całości w pamięci przez cały czas trwania programu.

Możemy jednak powielić podejście do usuwania elementów kolekcji, gdy są one używane. Możemy zachować ten sam "kształt kodu", tworząc generator, który opróżnia swoją kolekcję źródłową, gdy działa. Byłoby to wyglądać mniej więcej tak:

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) 

Teraz można łatwo zastąpić drain(collection) dla collection gdy chcesz, aby zwolnić pamięć jak przejść. Po wyczerpaniu drain(collection) obiekt collection będzie pusty.

2

Jeśli Twój algorytm zależy od pop-lingu od lewej strony lub od początku listy, możesz użyć obiektu deque z collections jako szybszej alternatywy.

Dla porównania:

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

Wydruki:

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

Więc dla tego konkretnego testu pojawiały się od początku listy lub kolejki, deque jest ponad 10x szybciej.

Ta duża zaleta dotyczy jednak tylko lewej strony. Jeśli wykonasz ten sam test za pomocą pop(), obie prędkości będą mniej więcej takie same. Możesz również odwrócić listę w miejscu i pop od prawej strony, aby uzyskać takie same wyniki, jak popleft z deque.


Pod względem "efektywności" znacznie wydajniejsze będzie przetwarzanie pojedynczych rzędów z bazy danych. Jeśli to nie jest opcja, przetwórz swoją listę (lub usuń) "zbiór" w miejscu.

Spróbuj czegoś podobnego.

Najpierw wyrwać przetwarzania rzędu:

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)  

Teraz spójrz na użyciu deque, aby umożliwić szybki wyskakuje od lewej:

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

lub jeśli chcesz trzymać się listy:

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

myślę, że oryginalne podejście popu (0), procesu wiersz, a co nazywamy GC 5000 wierszy jest pr wyraźnie nieoptymalny. W każdym razie gc będzie automatycznie wywoływany znacznie częściej.

Moja ostatnia rekomendacja:

  1. Użyj deque. To po prostu jak list, ale szybciej na lewą stronę lub pops;
  2. Użyj popleft(), więc nie musisz odwracać listy (jeśli kolejność collection jest znacząca);
  3. Przetwórz kolekcję w miejscu jako generator;
  4. Wyrzuć pojęcie wywoływania gc, ponieważ nic dla ciebie nie robi.
  5. Wyrzuć 1-4 tutaj, jeśli możesz po prostu zadzwonić do db i zdobyć 1 wiersz i przetworzyć 1 wiersz na raz!
+2

Czy mogę dowiedzieć się więcej o przemijaniu w dół? – dawg