CPython:为什么 += for strings 会改变字符串变量的 id
CPython: Why does += for strings change the id of string variable
Cpython优化了字符串自增操作,在为字符串初始化内存时,程序会为其留出额外的扩展space,因此自增时,不会将原字符串复制到新位置。
我的问题是为什么字符串变量的id改变了。
>>> s = 'ab'
>>> id(s)
991736112104
>>> s += 'cd'
>>> id(s)
991736774080
为什么字符串变量的id变了
字符串是不可变的。在 str
上使用 +=
不是就地操作;它创建一个具有新内存地址的新对象,这是 id()
在 CPython 的实现下给出的内容。
对于 str
具体而言,__iadd__
未定义,因此操作回退到 __add__
或 __radd__
。有关详细信息,请参阅 Python 文档的 data model 部分。
>>> hasattr(s, '__iadd__')
False
您尝试触发的优化是 CPython 的一个实现细节,是一件非常微妙的事情:有许多细节(e.f。您正在经历的一个)可以阻止它。
要获得详细的解释,需要深入了解 CPython 的实现,所以首先我会尝试给出一个简单的解释,至少应该给出正在发生的事情的要点。血淋淋的细节将在第二部分突出显示重要的代码部分。
让我们看一下这个函数,它展示了 desired/optimized 行为
def add_str(str1, str2, n):
for i in range(n):
str1+=str2
print(id(str1))
return str1
调用它会导致以下输出:
>>> add_str("1","2",100)
2660336425032
... 4 times
2660336425032
2660336418608
... 6 times
2660336418608
2660336361520
... 6 times
2660336361520
2660336281800
and so on
即每添加 8 次就会创建一个新字符串,否则旧字符串(或我们将看到的内存)将被重用。第一个 id 只打印了 6 次,因为它在 unicode-object 的大小为 2 modulo 8 时开始打印(而不是在后面的情况下为 0)。
第一个问题是,如果字符串在 CPython 中是不可变的,那么如何(或更好的时间)更改它?显然,如果字符串绑定到不同的变量,我们不能更改它 - 但我们可以更改它,如果当前变量是唯一的引用 - 由于 CPython 的引用计数,可以很容易地检查它(它是此优化不适用于不使用引用计数的其他实现的原因。
让我们通过添加额外的引用来更改上面的函数:
def add_str2(str1, str2, n):
for i in range(n):
ref = str1
str1+=str2
print(id(str1))
return str1
调用它会导致:
>>> add_str2("1","2",20)
2660336437656
2660337149168
2660337149296
2660337149168
2660337149296
... every time a different string - there is copying!
这实际上解释了您的观察结果:
import sys
s = 'ab'
print(sys.getrefcount(s))
# 9
print(id(s))
# 2660273077752
s+='a'
print(id(s))
# 2660337158664 Different
您的字符串 s
是 interned (see for example 以获取有关字符串驻留和整数池的更多信息),因此 s
不仅是一个 "using" 这个字符串,因此也是这个字符串无法更改。
如果我们避免实习,我们可以看到,字符串被重用了:
import sys
s = 'ab'*21 # will not be interned
print(sys.getrefcount(s))
# 2, that means really not interned
print(id(s))
# 2660336107312
s+='a'
print(id(s))
# 2660336107312 the same id!
但是这种优化是如何进行的?
CPython 使用自己的内存管理 - the pymalloc allocator, which is optimized for small objects with short lifetimes. The used memory-blocks are multiple of 8
bytes, that means if allocator is asked for only 1 byte, still 8 bytes are marked as used (more precise because of the 8-byte aligment 返回的指针,其余 7 个字节不能用于其他对象)。
但是有函数PyMem_Realloc
:如果要求分配器将 1 字节块重新分配为 2 字节块,则无事可做 - 无论如何都有一些保留字节。
这样一来,如果字符串只有一个引用,CPython 可以要求分配器重新分配字符串并要求多一个字节。在 8 例中的 7 例中,分配器无事可做,附加字节几乎免费可用。
但是,如果字符串的大小变化超过 7 个字节,则必须进行复制:
>>> add_str("1", "1"*8, 20) # size change of 8
2660337148912
2660336695312
2660336517728
... every time another id
此外,pymalloc回落到PyMem_RawMalloc
,通常是C-runtime的内存管理器,上面对字符串的优化已经不可能了:
>>> add_str("1"*512, "1", 20) # str1 is larger as 512 bytes
2660318800256
2660318791040
2660318788736
2660318807744
2660318800256
2660318796224
... every time another id
实际上,每次重新分配后地址是否不同取决于C运行时的内存分配器及其状态。如果内存未进行碎片整理,那么 realloc
无需复制即可扩展内存的可能性很高(但在我的机器上并非如此,因为我进行了这些实验),另请参见 。
出于好奇,这里是 str1+=str2
操作的完整回溯,可以在 a debugger 中轻松跟踪:
事情就是这样:
+=
被编译为 BINARY_ADD
-optcode,当在 ceval.c
中求值时,有一个 hook/special handling for unicode objects(见 PyUnicode_CheckExact
):
case TARGET(BINARY_ADD): {
PyObject *right = POP();
PyObject *left = TOP();
PyObject *sum;
...
if (PyUnicode_CheckExact(left) &&
PyUnicode_CheckExact(right)) {
sum = unicode_concatenate(left, right, f, next_instr);
/* unicode_concatenate consumed the ref to left */
}
...
unicode_concatenate
ends up calling PyUnicode_Append
, which checks whether the left-operand is modifiable (which basically checks 只有一个引用,字符串未被保留,还有一些其他内容)并调整它的大小或创建一个新的 unicode 对象,否则:
if (unicode_modifiable(left)
&& ...)
{
/* append inplace */
if (unicode_resize(p_left, new_len) != 0)
goto error;
/* copy 'right' into the newly allocated area of 'left' */
_PyUnicode_FastCopyCharacters(*p_left, left_len, right, 0, right_len);
}
else {
...
/* Concat the two Unicode strings */
res = PyUnicode_New(new_len, maxchar);
if (res == NULL)
goto error;
_PyUnicode_FastCopyCharacters(res, 0, left, 0, left_len);
_PyUnicode_FastCopyCharacters(res, left_len, right, 0, right_len);
Py_DECREF(left);
...
}
unicode_resize
ends up calling resize_compact
(mostly because in our case we have only ascii-characters), which ends up 调用 PyObject_REALLOC
:
...
new_unicode = (PyObject *)PyObject_REALLOC(unicode, new_size);
...
基本上会调用 pymalloc_realloc
:
static int
pymalloc_realloc(void *ctx, void **newptr_p, void *p, size_t nbytes)
{
...
/* pymalloc is in charge of this block */
size = INDEX2SIZE(pool->szidx);
if (nbytes <= size) {
/* The block is staying the same or shrinking.
....
*newptr_p = p;
return 1; // 1 means success!
...
}
...
}
其中 INDEX2SIZE
只是四舍五入到最接近的 8 的倍数:
#define ALIGNMENT 8 /* must be 2^N */
#define ALIGNMENT_SHIFT 3
/* Return the number of bytes in size class I, as a uint. */
#define INDEX2SIZE(I) (((uint)(I) + 1) << ALIGNMENT_SHIFT)
问了。
Cpython优化了字符串自增操作,在为字符串初始化内存时,程序会为其留出额外的扩展space,因此自增时,不会将原字符串复制到新位置。 我的问题是为什么字符串变量的id改变了。
>>> s = 'ab'
>>> id(s)
991736112104
>>> s += 'cd'
>>> id(s)
991736774080
为什么字符串变量的id变了
字符串是不可变的。在 str
上使用 +=
不是就地操作;它创建一个具有新内存地址的新对象,这是 id()
在 CPython 的实现下给出的内容。
对于 str
具体而言,__iadd__
未定义,因此操作回退到 __add__
或 __radd__
。有关详细信息,请参阅 Python 文档的 data model 部分。
>>> hasattr(s, '__iadd__')
False
您尝试触发的优化是 CPython 的一个实现细节,是一件非常微妙的事情:有许多细节(e.f。您正在经历的一个)可以阻止它。
要获得详细的解释,需要深入了解 CPython 的实现,所以首先我会尝试给出一个简单的解释,至少应该给出正在发生的事情的要点。血淋淋的细节将在第二部分突出显示重要的代码部分。
让我们看一下这个函数,它展示了 desired/optimized 行为
def add_str(str1, str2, n):
for i in range(n):
str1+=str2
print(id(str1))
return str1
调用它会导致以下输出:
>>> add_str("1","2",100)
2660336425032
... 4 times
2660336425032
2660336418608
... 6 times
2660336418608
2660336361520
... 6 times
2660336361520
2660336281800
and so on
即每添加 8 次就会创建一个新字符串,否则旧字符串(或我们将看到的内存)将被重用。第一个 id 只打印了 6 次,因为它在 unicode-object 的大小为 2 modulo 8 时开始打印(而不是在后面的情况下为 0)。
第一个问题是,如果字符串在 CPython 中是不可变的,那么如何(或更好的时间)更改它?显然,如果字符串绑定到不同的变量,我们不能更改它 - 但我们可以更改它,如果当前变量是唯一的引用 - 由于 CPython 的引用计数,可以很容易地检查它(它是此优化不适用于不使用引用计数的其他实现的原因。
让我们通过添加额外的引用来更改上面的函数:
def add_str2(str1, str2, n):
for i in range(n):
ref = str1
str1+=str2
print(id(str1))
return str1
调用它会导致:
>>> add_str2("1","2",20)
2660336437656
2660337149168
2660337149296
2660337149168
2660337149296
... every time a different string - there is copying!
这实际上解释了您的观察结果:
import sys
s = 'ab'
print(sys.getrefcount(s))
# 9
print(id(s))
# 2660273077752
s+='a'
print(id(s))
# 2660337158664 Different
您的字符串 s
是 interned (see for example s
不仅是一个 "using" 这个字符串,因此也是这个字符串无法更改。
如果我们避免实习,我们可以看到,字符串被重用了:
import sys
s = 'ab'*21 # will not be interned
print(sys.getrefcount(s))
# 2, that means really not interned
print(id(s))
# 2660336107312
s+='a'
print(id(s))
# 2660336107312 the same id!
但是这种优化是如何进行的?
CPython 使用自己的内存管理 - the pymalloc allocator, which is optimized for small objects with short lifetimes. The used memory-blocks are multiple of 8
bytes, that means if allocator is asked for only 1 byte, still 8 bytes are marked as used (more precise because of the 8-byte aligment 返回的指针,其余 7 个字节不能用于其他对象)。
但是有函数PyMem_Realloc
:如果要求分配器将 1 字节块重新分配为 2 字节块,则无事可做 - 无论如何都有一些保留字节。
这样一来,如果字符串只有一个引用,CPython 可以要求分配器重新分配字符串并要求多一个字节。在 8 例中的 7 例中,分配器无事可做,附加字节几乎免费可用。
但是,如果字符串的大小变化超过 7 个字节,则必须进行复制:
>>> add_str("1", "1"*8, 20) # size change of 8
2660337148912
2660336695312
2660336517728
... every time another id
此外,pymalloc回落到PyMem_RawMalloc
,通常是C-runtime的内存管理器,上面对字符串的优化已经不可能了:
>>> add_str("1"*512, "1", 20) # str1 is larger as 512 bytes
2660318800256
2660318791040
2660318788736
2660318807744
2660318800256
2660318796224
... every time another id
实际上,每次重新分配后地址是否不同取决于C运行时的内存分配器及其状态。如果内存未进行碎片整理,那么 realloc
无需复制即可扩展内存的可能性很高(但在我的机器上并非如此,因为我进行了这些实验),另请参见
出于好奇,这里是 str1+=str2
操作的完整回溯,可以在 a debugger 中轻松跟踪:
事情就是这样:
+=
被编译为 BINARY_ADD
-optcode,当在 ceval.c
中求值时,有一个 hook/special handling for unicode objects(见 PyUnicode_CheckExact
):
case TARGET(BINARY_ADD): {
PyObject *right = POP();
PyObject *left = TOP();
PyObject *sum;
...
if (PyUnicode_CheckExact(left) &&
PyUnicode_CheckExact(right)) {
sum = unicode_concatenate(left, right, f, next_instr);
/* unicode_concatenate consumed the ref to left */
}
...
unicode_concatenate
ends up calling PyUnicode_Append
, which checks whether the left-operand is modifiable (which basically checks 只有一个引用,字符串未被保留,还有一些其他内容)并调整它的大小或创建一个新的 unicode 对象,否则:
if (unicode_modifiable(left)
&& ...)
{
/* append inplace */
if (unicode_resize(p_left, new_len) != 0)
goto error;
/* copy 'right' into the newly allocated area of 'left' */
_PyUnicode_FastCopyCharacters(*p_left, left_len, right, 0, right_len);
}
else {
...
/* Concat the two Unicode strings */
res = PyUnicode_New(new_len, maxchar);
if (res == NULL)
goto error;
_PyUnicode_FastCopyCharacters(res, 0, left, 0, left_len);
_PyUnicode_FastCopyCharacters(res, left_len, right, 0, right_len);
Py_DECREF(left);
...
}
unicode_resize
ends up calling resize_compact
(mostly because in our case we have only ascii-characters), which ends up 调用 PyObject_REALLOC
:
...
new_unicode = (PyObject *)PyObject_REALLOC(unicode, new_size);
...
基本上会调用 pymalloc_realloc
:
static int
pymalloc_realloc(void *ctx, void **newptr_p, void *p, size_t nbytes)
{
...
/* pymalloc is in charge of this block */
size = INDEX2SIZE(pool->szidx);
if (nbytes <= size) {
/* The block is staying the same or shrinking.
....
*newptr_p = p;
return 1; // 1 means success!
...
}
...
}
其中 INDEX2SIZE
只是四舍五入到最接近的 8 的倍数:
#define ALIGNMENT 8 /* must be 2^N */
#define ALIGNMENT_SHIFT 3
/* Return the number of bytes in size class I, as a uint. */
#define INDEX2SIZE(I) (((uint)(I) + 1) << ALIGNMENT_SHIFT)
问了。