Google App Engine中的内存使用量出乎意料地高

时间:2012-03-03 12:50:33

标签: python google-app-engine

我有一个Python GAE应用程序,可以在每个实例中存储数据,内存使用量远高于我的预期。作为示例,请考虑我已添加到我的应用程序中的此测试代码:

from google.appengine.ext import webapp

bucket = []

class Memory(webapp.RequestHandler):
    def get(self):
        global bucket
        n = int(self.request.get('n'))
        size = 0
        for i in range(n):
            text = '%10d' % i
            bucket.append(text)
            size += len(text)
        self.response.out.write('Total number of characters = %d' % size)

使用查询变量n的值调用此处理程序将导致实例将n个字符串添加到其列表中,每个字符长10个字符。

如果我用n = 1调用它(为了加载所有东西),然后检查生产服务器上的实例内存使用情况,我看到一个29.4MB的数字。如果我然后用n = 100000调用它并再次检查,内存使用率已跃升至38.9MB。也就是说,我的内存占用量增加了9.5MB,只能存储一百万个字符,几乎是我预期的十倍。我相信角色每个只消耗一个字节,但即使这是错误的,还有很长的路要走。列表结构的开销肯定无法解释它。我尝试添加一个显式垃圾收集调用,但数字没有改变。我缺少什么,有没有办法减少占地面积?

(顺便说一下,我尝试使用set而不是列表,发现在使用n = 100000调用后,内存使用量增加了13MB。这表明100000字符串的设置开销比列表的开销多3.5MB。也比预期的要大得多。)

4 个答案:

答案 0 :(得分:1)

我知道我在这里的派对已经很晚了,但这一点都不奇怪......

考虑一个长度为1的字符串:

s = '1'

那个很小,对吗?也许某个地方大约1个字节?不。

>>> import sys
>>> sys.getsizeof('1')
38

因此,您创建的每个字符串都会产生大约37个字节的开销(所有这些字符串方法都需要存储在某处)。

此外,CPU通常最有效地根据“字大小”而不是字节大小来存储项目。在许多系统上,“字”是4个字节......)。我不确定,但是如果python的内存分配器也在那里使用技巧来保持它运行得相当快,我也不会感到惊讶。

另外,不要忘记列表被表示为过度分配的数组(以防止每次.append时出现巨大的性能问题)。有可能的是,当你创建一个100k元素的列表时,python实际上会分配110k或更多的指针。

最后,关于set - 这可能很容易解释为setlist更多地过度分配(他们需要避免所有这些哈希冲突) )。随着设置大小的增加,它们最终会在内存使用中出现跳跃,以便在阵列中有足够的空闲插槽以避免哈希冲突:

>>> sys.getsizeof(set([1]))
232
>>> sys.getsizeof(set([1, 2]))
232
>>> sys.getsizeof(set([1, 2, 3]))
232
>>> sys.getsizeof(set([1, 2, 3, 4]))
232
>>> sys.getsizeof(set([1, 2, 3, 4, 5]))
232
>>> sys.getsizeof(set([1, 2, 3, 4, 5, 6]))  # resize!
744

答案 1 :(得分:0)

列表结构的开销并没有解释你直接看到的内容,但memory fragmentation确实如此。并且字符串在底层内存方面具有非零开销,因此计算字符串长度将显着低估。

答案 2 :(得分:0)

我不是专家,但这是一个有趣的问题。看起来它更像是一个python内存管理问题,而不是GAE问题。您是否尝试在本地运行它并比较本地dev_appserver与GAE上部署的内存使用情况?这应该表明它是GAE平台,还是只是python。

其次,你使用的python代码很简单,但效率不高,列表理解而不是for循环应该更有效。这应该会减少内存使用量:

''.join([`%10d` % i for i in range(n)])

在幕后,您不断增加的字符串必须不断重新分配。每次通过for循环时,都会有一个丢弃的字符串。我希望在你的for循环之后触发垃圾收集器应该已经清理了额外的字符串。

在检查内存使用情况之前尝试触发垃圾收集器。

import gc
gc.collect()
return len(gc.get_objects())

这应该让你知道垃圾收集器是否还没有清除一些额外的字符串。

答案 3 :(得分:0)

这主要是对dragonx的回应。

示例代码仅用于说明问题,因此我并不关心效率低下。我反而担心为什么应用程序消耗的内存大约是实际数据的十倍。我可以理解有一些内存开销,但这么多?

尽管如此,我尝试使用列表理解(没有连接,以匹配我的原始),内存使用量略有增加,从9.5MB到9.6MB。也许那是在误差范围内。或者大范围()表达式可能会让它变得糟透了;毫无疑问,它已经发布,但我认为最好使用xrange()。通过连接,实例变量被设置为一个非常长的字符串,并且内存占用空间毫不奇怪地下降到合理的1.1MB,但这根本不是相同的情况。只需将实例变量设置为一百万个字符而不使用列表推导,即可获得相同的1.1MB。

我不确定我是否同意我的循环“有一个丢弃的字符串留在周围。”我相信字符串被添加到列表中(通过引用,如果说的那样),并且不会丢弃任何字符串。

我已经尝试过显式垃圾收集,正如我原来的问题所说。没有帮助。

这是一个有说服力的结果。将字符串的长度从10更改为其他数字会导致内存使用量发生成比例的变化,但也存在常量。我的实验表明,对于添加到列表中的每个字符串,无论字符串长度是多少,都有85字节的开销。这是字符串或将字符串放入列表的成本吗?我倾向于后者。创建100,000无的列表消耗4.5MB,或每个无45左右。这并不像字符串那么糟糕,但它仍然非常糟糕。正如我之前提到的,对于集合而言,它比列表更糟糕。

我希望我理解为什么开销(或碎片)是如此糟糕,但不可避免的结论似乎是大型小物件集合非常昂贵。你可能是对的,这更像是一个Python问题,而不是GAE问题。