【问题标题】:What happened when python reallocate object and how can I notice it?当 python 重新分配对象时发生了什么,我怎么能注意到它?
【发布时间】:2021-07-15 22:50:01
【问题描述】:

感谢您的光临。

一开始,我很好奇将对象无限附加到数组时发生了什么。

所以,我试过了。 append 3 to array until id of array changed


我的想法如下。

有一天,python 应该将数组重新分配到另一个地方(在堆段中)。然后虚拟内存地址也会改变。根据cpython源码中的注释,

[python/bltinmodule.c-1201th line]
这保证在同时存在的对象中是唯一的。(CPython使用对象的内存地址。)


这是我的测试代码
arr = [3]
prev_id = id(arr[0])
print('arr_id:',hex(id(arr)))
print('prev_id:',hex(prev_id))
i = 1
import time
start = time.time()
while prev_id == id(arr[0]):
    arr.append(3)
    if(i % 10000000 == 0):
        tmp = time.time()
        print(hex(id(arr)), hex(id(arr[-1])), i, tmp-start)
        start = tmp
    i += 1

print(id(arr))

结果是……

prev_id: 0x7f9d00c9f5a0
0x7f9d00c9f5a0 0x7f9d00c9f5a0 10000000 3.284541130065918
0x7f9d00c9f5a0 0x7f9d00c9f5a0 20000000 3.3981776237487793
0x7f9d00c9f5a0 0x7f9d00c9f5a0 30000000 3.1428868770599365
0x7f9d00c9f5a0 0x7f9d00c9f5a0 40000000 3.3757195472717285
0x7f9d00c9f5a0 0x7f9d00c9f5a0 50000000 3.1835339069366455
...
0x7f9d00c9f5a0 0x7f9d00c9f5a0 840000000 3.0633106231689453
0x7f9d00c9f5a0 0x7f9d00c9f5a0 850000000 3.1029069423675537
0x7f9d00c9f5a0 0x7f9d00c9f5a0 860000000 3.124239921569824
0x7f9d00c9f5a0 0x7f9d00c9f5a0 870000000 3.0969555377960205
0x7f9d00c9f5a0 0x7f9d00c9f5a0 880000000 3.0810909271240234
0x7f9d00c9f5a0 0x7f9d00c9f5a0 890000000 3.090634346008301
0x7f9d00c9f5a0 0x7f9d00c9f5a0 900000000 3.079714298248291
0x7f9d00c9f5a0 0x7f9d00c9f5a0 910000000 3.051016092300415
(the program infinitely append '3' to the array until run out of memory.)

第三列和第四列的图(x: 10,000,000*append, y: time(s))

比较'append(3)'和'append(1234567)'

在 colab 中记录消息。好像有些东西被重新分配了

[2021.04.23编辑三张图]



通过图形,我们可以注意到图形是周期性指向的。我认为这是因为 python 重新分配。

  1. 我猜对了吗?

另外,我实际上想获取数组的虚拟内存地址。可能是这个(0x7f9d00c9f5a0) 是吧。

  1. 那么(虽然数组被重新分配了)为什么数组的id是一样的呢?

数组重新分配后如何获取虚拟内存地址?

【问题讨论】:

  • NOTHING 永远不会改变现有对象的 ID。任何在大小上增长(因此最终需要重新分配内存)的东西,例如一个列表,必然会将增长的部分作为一个单独的内存块,由 ID 所指的固定大小的块指向。无法从纯 Python 中确定该单独块的地址。
  • 非常感谢杰森哈珀!顺便说一句,您能否深入解释一下“列表的增长部分必然会作为一个单独的内存块,由 ID 所指的固定大小的块所指向。”?
  • Python 列表对象本身不能在内存中移动(因为这会使对列表的所有现有引用无效)。但是列表数据必须能够在内存中移动(因为当列表增长时,通常不会有任何空间来扩展数据)。这不是唯一可以想到的解决方案,但将列表数据放在单独的内存块中肯定是最简单的解决方案。列表数据只有一个引用(在列表对象本身中),因此在需要重新分配时可以轻松更新。
  • 非常感谢杰森哈珀!

标签: python memory realloc


【解决方案1】:

CPython 只是聪明而已。

您正在附加 3,它实际上默认存储在 CPython 中的内存位置,以便更快地查找它。我忘记了默认存储了多少值,但您可以通过检查来验证这一点:

In [45]: a = [12]

In [46]: print(hex(id(12)), hex(id(a[-1])))  # Will be the same
0x7ffab6401800 0x7ffab6401800

In [47]: a = [1234]

In [48]: print(hex(id(1234)), hex(id(a[-1])))  # Will be different
0x18f41c9a490 0x18f41d88e10

如果你想检查这些范围,你可以这样做:

In [66]: def find_min_max():
    ...:     min_val = max_val = None
    ...:     for i in range(-1000, 1000):
    ...:         if i < 0 and min_val is None:
    ...:             if id(int(str(i))) == id(i):
    ...:                 min_val = i
    ...:         if i > 0 and max_val is None:
    ...:             if id(int(str(i))) != id(i):
    ...:                 max_val = i-1
    ...:     return min_val, max_val

In [67]: find_min_max()
Out[67]: (-5, 256)

所以在这种情况下,在我的系统上,也可能是你的系统上,[-5, 256] 将具有相同的 ID。由于您不断地附加 3,因此您基本上只是附加了相同的唯一内存地址,这很可能是这样,因此您的数组不需要重新分配。如果您想强制执行,请尝试附加 257 之类的内容,或超出存储值范围的内容。

编辑: 但是,id 不会改变。看看https://docs.python.org/3/library/functions.html#id

返回对象的“身份”。这是一个整数,保证在其生命周期内对于该对象是唯一且恒定的。生命周期不重叠的两个对象可能具有相同的 id() 值。

CPython 实现细节:这是对象在内存中的地址。

因为对象还活着,所以它会保留原来的id。所以没有办法从 Python 端看到重定位发生的情况。

编辑#2:

好的....所以你也许可以做到这一点。但这很复杂。从技术上讲,您仍然无法通过id 做到这一点,但您可以通过在 C 中构建自定义类型来添加自己的类型来做到这一点。Python 列表的代码可以在 here 中找到。具体来说,您感兴趣的是list_resize。不幸的是,我没有时间测试这个。但是你可以做的是要么在那里更改代码,这似乎很痛苦,因为你必须编译并拥有自己的 Python 版本,或者创建一个基本上包装 Python 列表的自定义类型。

为此,您需要了解 C 并包含 "python.h",但您应该能够创建新的自定义 PyListObject 并基本上复制现有功能,但还要添加另一个成员变量以显示内存地址列表。

例如:

typedef struct {
    PyObject_VAR_HEAD
    /* Vector of pointers to list elements.  list[0] is ob_item[0], etc. */
    PyObject **ob_item;

    Py_ssize_t allocated;
} PyListObject;

是默认值。

像这样添加你自己的:

```c
typedef struct {
    PyObject_VAR_HEAD
    /* Vector of pointers to list elements.  list[0] is ob_item[0], etc. */
    PyObject **ob_item;
    PyObject *base_address;

    Py_ssize_t allocated;
} PyListObject;

然后在你的新list_resize

static int
list_resize(PyListObject *self, Py_ssize_t newsize)
{
    PyObject **items;
    size_t new_allocated, num_allocated_bytes;
    Py_ssize_t allocated = self->allocated;

    if (allocated >= newsize && newsize >= (allocated >> 1)) {
        assert(self->ob_item != NULL || newsize == 0);
        Py_SET_SIZE(self, newsize);
        return 0;
    }

    new_allocated = ((size_t)newsize + (newsize >> 3) + 6) & ~(size_t)3;
   
    if (newsize - Py_SIZE(self) > (Py_ssize_t)(new_allocated - newsize))
        new_allocated = ((size_t)newsize + 3) & ~(size_t)3;

    if (newsize == 0)
        new_allocated = 0;
    num_allocated_bytes = new_allocated * sizeof(PyObject *);
    
    /* PREVIOUS IMPLEMENTATION */
    // items = (PyObject **)PyMem_Realloc(self->ob_item, num_allocated_bytes);
    /* NEW IMPLEMENTATION*/
    void* addr = PyMem_Realloc(self->ob_item, num_allocated_bytes);
    self->base_address = (PyObject*)addr;
    items = (PyObject**)addr;

    if (items == NULL) {
        PyErr_NoMemory();
        return -1;
    }
    self->ob_item = items;
    Py_SET_SIZE(self, newsize);
    self->allocated = new_allocated;
    return 0;
}

【讨论】:

  • 感谢 Chrispresso :) 我故意使用值 '3'。起初我附加了'i'(在我的代码中)。但是,我将其更改为 3 以消除分配大于 256 的数字的副作用。另外,我认为附加相同的唯一内存地址不能成为“不需要重新分配数组”的理由。我错了吗?
  • 另外,我做了另一个附加“1234567”的实验。您可以在我的问题正文中查看图表和日志消息(我编辑了它)。在额外的实验中, id 没有像你提到的那样改变。但是,正如您在图中看到的那样,有一些点,并且日志消息说有一个很大的分配。这不是数组重定位的结果吗? (也许是因为缓存大小?)让我们再多分享一下我们的想法:)非常感谢!欢呼X
  • id 永远不会改变,即使底层内存被重新分配。如果它改变了 Python 中任何对它的引用,则需要更新。
  • 感谢克里斯普里索。我会再考虑一下。你给了我经历的力量:)
  • @Youseop 我更新了一种方法,你可以从技术上获取 Python 对象的内存地址,但这需要很多工作;)
猜你喜欢
  • 2016-04-29
  • 1970-01-01
  • 2015-02-14
  • 1970-01-01
  • 2012-03-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-06-22
相关资源
最近更新 更多