python效率和内存中的大对象

时间:2013-04-05 02:50:03

标签: python memory-management garbage-collection

我有多个进程,每个进程处理有40000个元组的列表。这几乎可以最大化机器上的可用内存。如果我这样做:

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

这会释放更多内存吗?

2 个答案:

答案 0 :(得分:7)

由于collection正在以rows增长的速度收缩,因此您的内存使用量将保持稳定。 gc.collect()电话不会产生太大影响。

CPython中的内存管理很微妙。仅仅因为删除引用并运行收集周期并不一定意味着内存将返回到OS。请参阅this answer for details

要真正节省内存,您应该围绕生成器和迭代器而不是大型项列表来构造此代码。我很惊讶你说你有连接超时,因为获取所有行不应该花费比一次取一行并执行你正在进行的简单处理花费更多的时间。也许我们应该看看你的db-fetching代码?

如果真的不可能进行一次一行的处理,那么至少要将数据保存为不可变的双端队列,并使用生成器和迭代器对其执行所有处理。

我将概述这些不同的方法。

首先,一些常见功能:

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

现在理想的是一次一行的处理,如下所示:

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

这将占用尽可能少的内存 - 一次只能占用一行。

如果您确实需要存储整个结果集,可以稍微修改一下代码:

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)

现在我们首先将所有结果收集到collection中,这些结果完全保留在整个程序运行的内存中。

但是,我们也可以复制删除收集项目的方法。我们可以通过创建一个生成器来保持相同的“代码形状”,该生成器在其工作时清空其源集合。它看起来像这样:

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)

现在,当您想要释放内存时,可以轻松地将drain(collection)替换为collectiondrain(collection)耗尽后,collection对象将为空。

答案 1 :(得分:2)

如果您的算法依赖于从左侧或列表的开头弹出,您可以使用deque中的collections对象作为更快的替代方案。

作为比较:

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

打印:

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

因此,对于从列表或队列的开头弹出的特定测试,deque的速度提高了10倍以上。

然而,这个大优势仅适用于左侧。如果你用pop()运行同样的测试,两者的速度大致相同。您也可以将列表反转到右侧并从右侧弹出以获得与双端队列中的popleft相同的结果。


就“效率”而言,处理数据库中的单行会更有效率。如果这不是一个选项,请处理您的列表(或双端队列)'集合'。

尝试这些方法。

首先,打破行处理:

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)    

现在看一下使用双端队列来从左侧快速弹出:

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

或者如果你想坚持列表:

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

我认为你最初的pop(0)方法,处理行,每5000行调用一次gc可能不是最理想的。无论如何,gc将被自动调用的次数要多得多。

我的最终建议:

  1. 使用deque。它就像list,但左侧推或弹出更快;
  2. 使用popleft(),因此您无需撤消列表(如果collection的顺序有意义);
  3. 将您的收藏作为发电机处理;
  4. 抛出调用gc的概念,因为它没有为你做任何事情。
  5. 如果您可以调用数据库并获得1行并一次处理1行,则在此处抛出1-4!