当没有分配任何新内容时,Pypy 内存使用量会增加

Pypy memory usage increases when nothing new is allocated

我不确定这是否与其他 PyPy 内存问题重复,但在这里我将提供一个具体示例。

from __future__ import division

def mul_inv(a, m):
    """Modular multiplicative inverse, a^-1 mod m. Credit: rosettacode.org"""
    m0 = m
    x0, x1 = 0, 1
    if m == 1: return 1
    while a > 1:
        assert m != 0, "a and m must be coprime"
        q = a // m
        a, m = m, a%m
        x0, x1 = x1 - q * x0, x0
    if x1 < 0: x1 += m0
    return x1


M = 1000000009
L = 10**8

bin2 = [0] * L
bin2[0] = 1
for n in range(L-1):
    bin2[n+1] = (bin2[n] * (4*n + 2) * mul_inv(n+1, M)) % M
    if n % 10**5 == 0: print(n, bin2[n])

print(bin2[:20])

在 python 3.6 中,程序最多使用 3-4 GB 并运行完成(Armin Rigo 的列表更改并没有显着改变这一点)。使用 python 2.7.13 运行 PyPy 5.10.0,程序很快达到 8 GB(我有多少 RAM)并冻结。即使使用 gc.collect() 调用程序也会在 n 大约为 3.5 * 10^7 时耗尽内存。

这个内存使用量来自哪里?唯一的大内存使用应该是将 bin2 初始化为 10^8 int 列表。在 mul_inv 中的所有局部变量都被垃圾收集的假设下,没有其他东西会增加内存使用量。

糟糕,这是对整数列表进行优化的糟糕情况。问题是这开始于一个整数列表:

bin2 = [0] * L

这在内部存储为一个整数数组。它通常更紧凑,即使在这种情况下它不会改变任何东西---因为在 CPython 上它是一个列表,其中包含同一对象 0.

L 个副本

但问题是很快,我们在列表中存储了一个long。此时,我们需要将整个列表变成可以存储任何东西的泛型。但!问题是我们看到了 1 亿个零,所以我们创建了 1 亿个 0 个对象。除了列表本身的 800MB 之外,这会立即产生 3 GB 的内存压力。

如果我们这样初始化列表,我们可以检查是否不会出现问题,因此它确实包含 1 亿次相同的对象:

bin2 = [0L] * L     # Python 2.x
bin2[0] = 1

就是说,在您的示例中,您不需要列表首先包含 1 亿个元素。您可以将其初始化为:

bin2 = [1]

并使用 bin2.append()。这让程序启动得更快,并且在开始时不会占用大量内存。

请注意 PyPy3 仍然比 CPython3 使用更多的内存。

AFAICT 这里的问题是您将 long 分配给数组,尽管您使用了模数,PyPy 似乎没有注意到该数字仍然适合机器字。

我可以想到两种方法来解决这个问题:

  1. 通过int()传递分配给bin2[n+1]的值。
  2. 使用array.array().

前者只影响 PyPy2,并导致我的 Mac 上的稳定内存占用约为 800MB,而无论我是否 [=28],后者似乎稳定在 ~1.4GB =] 它在 PyPy2 或 PyPy3 中。

我还没有 运行 程序完全完成,所以 YMMV…