【问题标题】:Memory allocation when casting to a list in Python在 Python 中转换为列表时的内存分配
【发布时间】:2021-10-08 16:26:55
【问题描述】:

我知道当你在 python 中追加到一个列表时,为列表分配的内存量会慢慢增长。当您达到预分配部分的限制时,将分配新内存并复制现有项目。

将迭代器强制转换为列表时会发生什么?它执行相同的过程还是更智能?

如果你将实现__len__ 方法的东西转换为列表,它是否分配了obj.__len__() 内存槽以减少转换过程中的复制量?

综上所述,以下三个sn-ps对于列表list_of_interest的内存是如何分配的有区别吗?

标准附加

list_of_interest = []
for i in range(1000):
    list_of_interest.append(i)

铸造一个迭代器

list_of_interest = list(range(1000))

使用__len__ 投射对象

b = tuple(range(1000))
list_of_interest = list(b)

感谢您的帮助!

【问题讨论】:

  • 这不是演员表(至少,在我认为该术语应该涵盖的任何意义上)。 list 使用给定的迭代器并生成 list 的新实例。
  • 但是 IIRC,list 将尝试立即为列表分配足够的空间,如果 __len__ 可用于提供该信息,而不是一次将一个元素附加到最初为空的列表。 (注意range定义了len,因为它可以准确计算半开区间[0, 1000)中有多少整数。)
  • 类型也可以提供__length_hint__ 方法,该方法可以提供估计大小,list 可以使用相同的方式至少预分配 一些 内存,这将预计会被使用。
  • 也许测试有点像this

标签: python big-o


【解决方案1】:

我们可以通过使用dis 模块反汇编每个代码示例来更深入地了解发生了什么。

标准追加:

dis.dis("""
list_of_interest = []
for i in range(1000):
    list_of_interest.append(i)
""")

给我们

  2           0 BUILD_LIST               0
              2 STORE_NAME               0 (list_of_interest)

  3           4 SETUP_LOOP              26 (to 32)
              6 LOAD_NAME                1 (range)
              8 LOAD_CONST               0 (1000)
             10 CALL_FUNCTION            1
             12 GET_ITER
        >>   14 FOR_ITER                14 (to 30)
             16 STORE_NAME               2 (i)

  4          18 LOAD_NAME                0 (list_of_interest)
             20 LOAD_METHOD              3 (append)
             22 LOAD_NAME                2 (i)
             24 CALL_METHOD              1
             26 POP_TOP
             28 JUMP_ABSOLUTE           14
        >>   30 POP_BLOCK
        >>   32 LOAD_CONST               1 (None)
             34 RETURN_VALUE

仔细观察,我们看到在循环中调用 append 方法(就像我们写的那样)。深入研究实现(here),我们看到这最终调用了app1,后者调用了list_resize,它过度分配以尝试减少重新分配。

另外两个例子:

正如其他答案中所述,range 实际上确实实现了__len__ 制作

list_of_interest = list(range(1000))

b = tuple(range(1000))
list_of_interest = list(b)

工作方式非常相似。

拆解第一个给出:

  1           0 LOAD_NAME                0 (list)
              2 LOAD_NAME                1 (range)
              4 LOAD_CONST               0 (1000)
              6 CALL_FUNCTION            1
              8 CALL_FUNCTION            1
             10 STORE_NAME               2 (list_of_interest)
             12 LOAD_CONST               1 (None)
             14 RETURN_VALUE

第二个:

  2           0 LOAD_NAME                0 (tuple)
              2 LOAD_NAME                1 (range)
              4 LOAD_CONST               0 (1000)
              6 CALL_FUNCTION            1
              8 CALL_FUNCTION            1
             10 STORE_NAME               2 (b)

  3          12 LOAD_NAME                3 (list)
             14 LOAD_NAME                2 (b)
             16 CALL_FUNCTION            1
             18 STORE_NAME               4 (list_of_interest)
             20 LOAD_CONST               1 (None)
             22 RETURN_VALUE

在这两种情况下,我们都调用定义为herelist 构造函数。我们看到,如果提供给list 的可迭代对象有一个长度,那么我们调用list_preallocate_exact,它会按照您的预期执行。否则我们在一个空列表上调用list_extend。如果我们的迭代器不是列表或元组(在这种情况下我们知道它不是),我们将调用PyObject_LengthHint 来猜测迭代器的长度并以此方式预分配空间。由于长度提示可能太大或太小,我们可能需要在以标准方式循环遍历迭代器时分配更多内存,并且在提示太大的情况下,我们可能会在完成循环后释放空间。

其他情况:

我们可以尝试其他示例,但我们会看到上面的 list 构造函数有几个特殊情况。通常我们接下来会去list_extend,它也有几个特殊情况。如果我们的迭代器没有实现长度或长度提示,we default to a length hint of 8。为什么是8?你得问维护者那个。

结论:

如果 Python 从一开始就可以知道结果列表所需的长度,那么您将只有一个分配。如果它必须猜测,您可能有不止一个或一个释放。如果在循环中追加或提供没有长度或长度提示的迭代器的情况下无法猜测,您将有多个分配。

【讨论】:

    【解决方案2】:

    当定义__len__ 时,list(至少在 CPython 中;其他实现可能不同)将使用可迭代对象报告的大小来预分配一个数组,该数组恰好足够容纳所有可迭代对象的元素。

    您的第二个和第三个示例实际上是相同的,因为range 确实提供了__len__(因为计算具有定义端点的范围内的整数数量很简单)。

    一个不能定义__len__的可迭代的例子是filter。即使filter 的实例知道 its 可迭代参数有多大,它也不知道在没有实际迭代的情况下会产生多少个参数。所以像list(filter(odd, range(1000))) 这样的东西必须从一个空列表开始并附加到它,因为filter 返回奇数。

    容器可以定义一个__length_hint__ 方法,至少可以估计它们包含多少项目。不过,我在listobject.c 中没有看到任何证据表明list 将利用它来预分配其数组。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-08-19
      • 2021-01-28
      • 1970-01-01
      • 2022-01-03
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多