【问题标题】:Python function slows down with presence of large listPython函数因存在大列表而变慢
【发布时间】:2011-08-14 23:21:19
【问题描述】:

我正在测试几种不同方法对我的一些数据进行复杂迭代的速度,我发现了一些奇怪的东西。似乎在某个函数本地有一个大列表会大大减慢该函数,即使它没有触及该列表。例如,通过相同生成器函数的 2 个实例创建 2 个独立列表,第二次的速度大约慢 2.5 倍。如果在创建第二个列表之前删除了第一个列表,则两个迭代器的速度相同。

def f():  
    l1, l2 = [], []  
    for c1, c2 in generatorFxn():  
        l1.append((c1, c2))  
    # destroying l1 here fixes the problem 
    for c3, c4 in generatorFxn():  
        l2.append((c3, c4))

每个列表最终都有大约 310 万个项目,但我也看到了较小列表的相同效果。第一个for 循环运行大约需要 4.5 秒,第二个需要 10.5 秒。如果我在注释位置插入l1= []l1= len(l1),两个for 循环都需要4.5 秒。

为什么函数中本地内存分配的速度与该函数变量的当前大小有关?

编辑: 禁用垃圾收集器可以解决所有问题,因此必须是由于它不断运行。结案!

【问题讨论】:

    标签: python performance memory allocation


    【解决方案1】:

    当您创建那么多新对象(300 万个元组)时,垃圾收集器就会陷入困境。如果您使用 gc.disable() 关闭垃圾收集,问题就会消失(并且程序在启动时运行速度提高了 4 倍)。

    【讨论】:

    • 是的,先生,禁用它会减少 4.5 秒至 1.3 秒,并且几乎消除了差异(第二个仍然稍慢,但不再慢很多)。为什么即使在创建了一个大列表并且只是存在并且没有被修改之后,垃圾收集器的运行速度如此之慢?函数返回后不应该只做工作吗?
    • 另外,如果l 是类变量而不是局部变量,为什么减速会消失?
    • 这是我的最佳猜测:垃圾收集器会定期运行以收集旧对象。它通过比较自上次收集以来已分配和已释放对象的数量来确定何时运行。如果allocated-deallocated > threshold,收集器运行(默认阈值=700)。由于您每次迭代创建(至少)1 个新对象,因此收集器运行 3e6/700 = 4285 次。这实际上减慢了两次迭代,但第二次迭代更慢,因为收集器要检查的对象更多。
    • 我将添加两个简单的建议来避免这个问题:1) 从 2 个列表开始并将值附加到这些列表上,而不是创建 300 万个元组。 2)在列表生成期间增加垃圾收集阈值(至少暂时)
    • 谢谢,这两个建议都很有意义,我会记得使用它们。此外,今天重新测试后,将l 设置为类变量似乎不再能解决问题。我昨天一定是测试出了问题。为帮助干杯!
    【解决方案2】:

    没有更详细的仪器是不可能的。

    作为一个非常非常初步的步骤,检查您的主内存使用情况。如果您的 RAM 已全部填满并且您的操作系统正在分页到磁盘,那么您的性能将非常糟糕。在这种情况下,您最好将中间产品放在内存以外的地方。如果您只需要顺序读取数据,请考虑写入普通文件;如果您的数据遵循严格的结构,请考虑持久化到关系数据库中。

    【讨论】:

    • 列表中的每一项都是由 2 个字符串组成的元组,每个字符串长度为 3 个字符。而且我在较小的列表中也看到了同样的效果;在每个列表大小为 770,000 时,时间分别为 0.57 和 0.96 秒。在 188,000 时,它们分别为 0.11 和 0.15 秒。我很确定这不是我跨越的内存阈值,因为我使用的是 64 位系统,内存为 4gb。
    【解决方案3】:

    我的猜测是,当创建第一个列表时,可用的内存更多,这意味着随着列表的增长需要重新分配的机会更少。

    在您使用第一个列表占用大量内存后,您的第二个列表具有更高的 由于 python 列表是动态调整大小的,因此随着它的增长需要重新分配的机会。

    【讨论】:

    • + 1 很有意义并提醒我内存分配(通常)不在 1 个大块中。根据 Activity Monitor 的说法,这两个列表总共需要大约 280mb 的空间,所以这是一个相当大的块。
    【解决方案4】:

    在函数返回之前,函数本地数据使用的内存不会被垃圾回收。除非您需要进行切片,否则对大量数据使用列表并不是一个好主意。

    从您的示例来看,创建这些列表的目的并不完全清楚。您可能需要考虑使用生成器而不是列表,尤其是在列表将被迭代的情况下。如果您需要对返回数据进行切片,请在那时将生成器转换为列表。

    【讨论】:

    • 创建这些列表最初只是为了测试不同生成器函数的速度,但是一旦找到合适的算法,我将不得不创建这样的列表。问题不在于找到更好的数据存储方式,而在于为什么会出现如此严重的性能损失。这对我来说毫无意义,我想在将来编程时牢记答案。而且我知道垃圾收集不会在函数中间发生,这就是为什么重新分配 l 不应该解决问题(但确实如此)。
    猜你喜欢
    • 1970-01-01
    • 2022-01-22
    • 2016-09-19
    • 1970-01-01
    • 2012-03-25
    • 1970-01-01
    • 2012-04-01
    • 2015-09-12
    • 1970-01-01
    相关资源
    最近更新 更多