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.
Nie chcę być chytrym, ale czemu nie spróbujesz tego i nie zobaczysz? –
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
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