【问题标题】:Generator using item[n-1] + item[n] memory使用 item[n-1] + item[n] 内存的生成器
【发布时间】:2014-03-14 06:30:13
【问题描述】:

我有一个 Python 3.x 程序,它处理几个包含大量数据的大型文本文件,这些数据有时会超出我微不足道的工作站的内存限制。从一些基本的内存分析来看,似乎在使用生成器时,我的脚本的内存使用量激增以容纳连续的元素,使用的内存是我预期的两倍。

我制作了一个简单的独立示例来测试生成器,我在 Python 2.7、3.3 和 3.4 中得到了类似的结果。我的测试代码如下,memory_usage()this function from an SO question 的修改版本,它使用/proc/self/status 并在我观看时与top 一致。 resource 可能是一种更跨平台的方法:

import sys, resource, gc, time

def biggen():
    sizes = 1, 1, 10, 1, 1, 10, 10, 1, 1, 10, 10, 20, 1, 1, 20, 20, 1, 1
    for size in sizes:
        data = [1] * int(size * 1e6)
        #time.sleep(1)
        yield data

def consumer():
    for data in biggen():
        rusage = resource.getrusage(resource.RUSAGE_SELF)
        peak_mb = rusage.ru_maxrss/1024.0
        print('Peak: {0:6.1f} MB, Data Len: {1:6.1f} M'.format(
                peak_mb, len(data)/1e6))
        #print(memory_usage()) # 

        data = None  # go
        del data     # away
        gc.collect() # please.

# def memory_usage():
#     """Memory usage of the current process, requires /proc/self/status"""
#     # https://stackoverflow.com/a/898406/194586
#     result = {'peak': 0, 'rss': 0}
#     for line in open('/proc/self/status'):
#         parts = line.split()
#         key = parts[0][2:-1].lower()
#         if key in result:
#             result[key] = int(parts[1])/1024.0
#     return 'Peak: {peak:6.1f} MB, Current: {rss:6.1f} MB'.format(**result)

print(sys.version)
consumer()

在实践中,我将处理来自这种生成器循环的数据,只保存我需要的数据,然后丢弃它。

当我运行上述脚本时,两个大元素串联在一起(数据大小可能变化很大),似乎 Python 在释放前一个之前计算下一个,导致内存使用量增加一倍。

$ python genmem.py 
2.7.3 (default, Sep 26 2013, 20:08:41) 
[GCC 4.6.3]
Peak:    7.9 MB, Data Len:    1.0 M
Peak:   11.5 MB, Data Len:    1.0 M
Peak:   45.8 MB, Data Len:   10.0 M
Peak:   45.9 MB, Data Len:    1.0 M
Peak:   45.9 MB, Data Len:    1.0 M
Peak:   45.9 MB, Data Len:   10.0 M
#        ^^  not much different versus previous 10M-list
Peak:   80.2 MB, Data Len:   10.0 M
#        ^^  same list size, but new memory peak at roughly twice the usage
Peak:   80.2 MB, Data Len:    1.0 M
Peak:   80.2 MB, Data Len:    1.0 M
Peak:   80.2 MB, Data Len:   10.0 M
Peak:   80.2 MB, Data Len:   10.0 M
Peak:  118.3 MB, Data Len:   20.0 M
#        ^^  and again...  (20+10)*x
Peak:  118.3 MB, Data Len:    1.0 M
Peak:  118.3 MB, Data Len:    1.0 M
Peak:  118.3 MB, Data Len:   20.0 M
Peak:  156.5 MB, Data Len:   20.0 M
#        ^^  and again. (20+20)*x
Peak:  156.5 MB, Data Len:    1.0 M
Peak:  156.5 MB, Data Len:    1.0 M

data = Nonedel datagc.collect() 的疯狂传送带和吊带和管道胶带方法无济于事。

我很确定生成器本身不会在内存上加倍,因为否则它产生的单个大值会增加峰值使用率,并且在相同的迭代中出现了一个大对象;它只是大的连续对象。

如何保存我的记忆?

【问题讨论】:

  • id_ = None 没用,因为 _id 被 ids 引用。
  • 如果你只关心data 的第一个元素,你应该重构plate.good_data 给你一个生成器,然后只获取它产生的第一个元素,不是吗?似乎您所有的问题都来自将大量 data 加载到内存中,其中绝大多数您并不关心。
  • 如何为idata 使用临时列表?类似:for [i, data] in enumerate(plate.good_data())。垃圾收集器有可能在这里做点什么吗?
  • @roippi 正在使用中,我会将data 传递给另一个函数,该函数将其分解为更易于管理的东西。我试图逐步制作更多最小的“程序”来缩小问题的范围,即使没有保留对它的任何引用(我可以看到),它仍然会占用内存。
  • @roippi 我也许可以修改我的生成器以接受一个在返回项目之前执行上述处理的函数......

标签: python memory-management


【解决方案1】:

您是否尝试过使用gc 模块?在那里你可以在循环之间get a list of the objects that still reference your large data,检查它是否在list of unreachable but unfreed objects中,或者启用一些调试标志。

如果运气好的话,在每个循环之后对gc.collect() 的简单调用可能会在一行中解决您的问题。

【讨论】:

  • 好主意,但我得到了与collect() 相同的结果(我忘了提及,但我更新了我的帖子)。我会看看让它告诉我什么可能会保留 ref。
  • 我更新了问题以提供任何人都可以运行的示例。
【解决方案2】:

问题出在生成器函数中;特别是在声明中:

    data = [1] * int(size * 1e6)

假设您在 data 变量中有旧内容。当您运行此语句时,它首先计算结果,因此您在内存中有 2 个这样的数组;旧的和新的。只有这样 data 变量才被更改为指向新结构,旧结构被释放。尝试将迭代器函数修改为:

def biggen():
    sizes = 1, 1, 10, 1, 1, 10, 10, 1, 1, 10, 10, 20, 1, 1, 20, 20, 1, 1
    for size in sizes:
        data = None
        data = [1] * int(size * 1e6)
        yield data

【讨论】:

    【解决方案3】:

    代替:

            data = [1] * int(size * 1e6)
            #time.sleep(1)
            yield data
    

    试试:

            yield [1] * int(size * 1e6)
    

    问题只是生成器的data 局部变量保留了对已生成列表的引用,从而防止它被垃圾收集,直到生成器恢复并丢弃该引用。

    换句话说,在生成器之外执行del data 对垃圾回收没有影响,除非这是对数据的唯一引用。避免在生成器内部进行引用使这一点成为现实。

    附录

    如果您必须操作数据,首先,您可以使用这样的 hack 在生成引用之前删除它:

            data = [1] * int(size * 1e6)
            # ... do stuff with data ...
    
            # Yield data without keeping a reference to it:
            hack = [data]
            del data
            yield hack.pop()
    

    【讨论】:

    • 我现在在你和 ondra 的帖子之间看到了。通过我的简化,很容易使修复看起来微不足道,但我的实际代码使它变得更加困难,因为我对数据进行其他操作并具有顺序生成器。
    • @NickT:我添加了一个快速破解示例,让您可以使用局部变量,但在屈服时仍然删除引用。除此之外,您可能还需要展示您的实际代码以获得进一步的建议。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-07-29
    • 1970-01-01
    • 1970-01-01
    • 2012-01-03
    • 2014-11-30
    • 1970-01-01
    相关资源
    最近更新 更多