Python源码剖析 第三章 字符串对象

本章也是了解字符串对象的创建,释放过程

本质上与int对象类似, 换汤不换药, Python会缓存特定的字符串对象在内存中, 这部分字符串对象可被共享(这种共享机制称为intern), 这类被共享的字符串对象采用的技术与小整数对象类似

对于不采用intern机制的字符串对象, 则正常的创建/释放

唯一不同的区别是:

  • 字符串对象是变长对象, 即字符串对象在被创建前,其存储的字符长度是未知的, 但是Python中的每类对象的大小是固定的, 所以一个字符串对象由两部分内存组成, 一部分内存用来存储字符串对象, 这部分内存大小是固定不变的,一部分内存是用来存储字符串对象的所有字符, 这部分内存是不固定的, 因此字符串的内存复用策略与int这类定长对象有细微不同

创建PyStringObject对象

PyStringObject对象结构

PyStringObject对象结构如下:

1
2
3
4
5
6
7
[stringobject.h]
typedef struct {
PyObject_VAR_HEAD
long ob_shash;
int ob_sstate;
char ob_sval[1];
} PyStringObject;

替换掉宏, 实际上结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[stringobject.h]
typedef struct {
int ob_refcnt; // 引用计数
struct _typeobject *ob_type;
// ob_size 这边用来指明存储字符的那一部分数组(ob_sval)的长度
int ob_size; /* Number of items in variable part */
long ob_shash; // 存储hash值, 初始值为-1
int ob_sstate; // 标记了该对象是否已经过intern机制的处理
char ob_sval[1]; // ob_sval 存储有一段字符数组, 在这里作用类似于指针, 用于拿到所存储的字符串
} PyStringObject;


/* Invariants:
* ob_sval contains space for 'ob_size+1' elements.
* ob_sval[ob_size] == 0.
* ob_shash is the hash of the string or -1 if not computed yet.
* ob_sstate != 0 if the string object is in stringobject.c's
* 'interned' dictionary; in this case the two references
* from 'interned' to this object are *not counted* in ob_refcnt.
*/

Python利用ob_sval 和 ob_size 来管理所存储的字符串

  • ob_sval 指向一段长度为ob_size+1字节大小的内存, 其中ob_sval[ob_size] == ‘\0’
1
2
3
4
5
6
7
8
9
10

/* Caching the hash (ob_shash) saves recalculation of a string's hash value.
Interning strings (ob_sstate) tries to ensure that only one string
object with a given value exists, so equality tests can be one pointer
comparison. This is generally restricted to strings that "look like"
Python identifiers, although the intern() builtin can be used to force
interning of any string.
Together, these sped the interpreter by up to 20%. */

// 缓存hash值, 和加入intern机制, 使得Python的虚拟机的执行效率提升20%!!

PyString_Type 类型对象

下面列出了PyStringObject对应的类型对象——PyString_Type

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[stringobject.c]
PyTypeObject PyString_Type = {
PyObject_HEAD_INIT(&PyType_Type)
0,
"str",
// Py_ssize_t tp_basicsize, tp_itemsize; /* For allocation */
// tp_basicsize 对应sizeof(PyStringObject), tp_itemsize 对应sizeof(char)
sizeof(PyStringObject),
sizeof(char),
……
(reprfunc)string_repr, /* tp_repr */
&string_as_number, /* tp_as_number */
&string_as_sequence, /* tp_as_sequence */
&string_as_mapping, /* tp_as_mapping */
(hashfunc)string_hash, /* tp_hash */
0, /* tp_call */
……
string_new, /* tp_new */
PyObject_Del, /* tp_free */
};

类型的元信息中, 有tp_basicsize和tp_itemsize, 这个与内存分配有关

  • tp_basicsize 表示这个类型的对象所占用的内存大小
  • tp_itemsize 用于变长对象, 表示可变内存那部分基本内存单位大小,对于Python中的任何一种变长对象,tp_itemsize这个域是必须设置的, 这个tp_itemsize和ob_size共同决定了应该额外申请的内存之总大小是多少

PyStringObject的类型对象中 可以看到tp_itemsize 被设置为sizeof(char), 即可变内存中以字符大小为基本单位

可以对比Int类型的元信息:

可以看到tp_itemsize 为0

1
2
3
4
5
6
7
8
PyTypeObject PyInt_Type = {
PyObject_HEAD_INIT(&PyType_Type)
0,
"int",
sizeof(PyIntObject),
0, // tp_itemsize 为0
...
}

创建字符串对象

对字符串的结构都了解了, 接下来就是要了解如何创建一个字符串对象

1
2
#define PyObject_INIT_VAR(op, typeobj, size) \
( (op)->ob_size = (size), PyObject_INIT((op), (typeobj)) )
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
[stringobject.c]

static PyStringObject *characters[UCHAR_MAX + 1]; // 用于存储单字符对象
static PyStringObject *nullstring;
#define SSTATE_NOT_INTERNED 0 // 未被intern的字符串对象
#define SSTATE_INTERNED_MORTAL 1 // 当被intern的字符串对象,引用计数为0的时候, 会被销毁
#define SSTATE_INTERNED_IMMORTAL 2 // 该状态的字符串对象永远不会被销毁, 与Python虚拟机同年同月同日死

#define UCHAR_MAX 255 /* max value for an unsigned char */

PyObject* PyString_FromString(const char *str)
{
register size_t size;
register PyStringObject *op;

//[1]: 判断字符串长度
size = strlen(str);
// PY_SSIZE_T_MAX是一个与平台相关的值,在WIN32系统下,该值为2 147 483 647。换算一下,就是2GB
if (size > PY_SSIZE_T_MAX) {
return NULL;
}

//[2]: 处理null string
if (size == 0 && (op = nullstring) != NULL) {
return (PyObject *)op;
}
//[3]: 处理字符
// 对于单个字符, 所以ASCII值为0~255, 直接从characters这个数组中拿到并返回(这个类似于小整数对象)
if (size == 1 && (op = characters[*str & UCHAR_MAX]) != NULL) {
return (PyObject *)op;
}

/* [4]: 创建新的PyStringObject对象,并初始化 */
/* Inline PyObject_NewVar */
// 分配字符串对象内存+额外size个字符大小内存用于存储字符串值
op = (PyStringObject *)PyObject_MALLOC(sizeof(PyStringObject) + size);
PyObject_INIT_VAR(op, &PyString_Type, size); // 执行PyObject_INIT和给ob_size赋值
op->ob_shash = -1;
op->ob_sstate = SSTATE_NOT_INTERNED; // 值为0
// 拷贝字符串str到ob_sval 数组里面
memcpy(op->ob_sval, str, size+1);

/* share short strings */
// 这里就是Python intern共享机制的实现, 后面会详细解析
if (size == 0) {
// 空字符串的处理
PyObject *t = (PyObject *)op;
PyString_InternInPlace(&t);
op = (PyStringObject *)t;
nullstring = op;
Py_INCREF(op);
} else if (size == 1) {
PyObject *t = (PyObject *)op;
PyString_InternInPlace(&t);
op = (PyStringObject *)t;
characters[*str & UCHAR_MAX] = op;
Py_INCREF(op);
}
return (PyObject *) op;
return (PyObject *) op;
}

创建不同长度的字符串对象的区别

总的来说针对不同长度的字符串, 具有不同的处理流程, 先暂时忽略intern机制,后续会细讲

主要区别如下:

  • 创建空字符串对象
1
static PyStringObject *nullstring;

Python会静态初始化nullstring 字符串对象指针, 用来指向空字符串对象, 初始值为null

首次创建空字符串对象时, 由于nullstring 值为null, 所以Python会创建一个空字符串对象, 并intering在dict中, 以后再次创建一个空字符串对象时,只需返回nullstring指向的对象即可

  • 创建单个字符的字符串对象
1
2
#define	UCHAR_MAX	255		/* max value for an unsigned char */
static PyStringObject *characters[UCHAR_MAX + 1];

Python 同样会静态初始化一个characters数组,用于存储所有指向单个字符对象的字符串指针

字符的索引值即表示该字符的ASCII码值

当Python首次创建单字符字符串对象时, characters数组中还未存储有该对象的指针, 因此Python会创建后,并interning该对象, 等之后再次创建这个对象时,直接从characters数组中返回即可

  • 创建多个字符的字符串对象

对于多个字符串的字符串对象, Python的有选择性的interning对象

创建一个字符串对象的通用流程

在创建一个字符串对象的时候

Python会初始化两部分内存, 一部分用于存储固定大小的字符串对象, 另一部分用于存储变长的字符串

image-20190505232307035

上面图片展示的是一个”Python”字符串对象的内部存储结构

ob_size 为 6 表示字符串长度为6

ob_hash为-1 表示 还未缓存hash值

ob_sstate 为0 表示还未被intern

ob_sval 指向P字符

Python的内存管理

1
2
3
4
5
6
7
/* PyMem_MALLOC(0) means malloc(1). Some systems would return NULL
for malloc(0), which would be treated as an error. Some platforms
would return a pointer with no memory behind it, which would break
pymalloc. To solve these problems, allocate an extra byte. */
#define PyMem_MALLOC(n) malloc((n) ? (n) : 1)
#define PyMem_REALLOC(p, n) realloc((p), (n) ? (n) : 1)
#define PyMem_FREE free
1
2
3
4
/* ! WITH_PYMALLOC */
#define PyObject_MALLOC PyMem_MALLOC
#define PyObject_REALLOC PyMem_REALLOC
#define PyObject_FREE PyMem_FREE

这里提下Python的内存管理方法是基于C中的内存管理封装而成

字符串的intern共享机制的实现

1
2
3
4
5
6
7
8
9
/* This dictionary holds all interned strings.  Note that references to
strings in this dictionary are *not* counted in the string's ob_refcnt.
When the interned string reaches a refcnt of 0 the string deallocation
function will delete the reference from this dictionary.

Another way to look at this is that to say that the actual reference
count of a string is: s->ob_refcnt + (s->ob_sstate?2:0)
*/
static PyObject *interned;

可以先看下官方对intern机制的解释:

  • Python将需要intern(共享)的字符串对象存储在interned字典中
  • 应该要忽略interned字典对被缓存的字符串的引用
    • 实际上字符串的引用计数值的计算方法为: s->ob_refcnt + (s->ob_sstate?2:0) !!!
      • 可以看到interned字典对被缓存的对象的引用计数有两个, 这是因为interned字典的key和value都是被缓存对象的指针
  • 当被存储的字符串对象引用计数变为0的时候(需要忽略intern字典的两个引用),从字典中删除
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[stringobject.c]
PyObject* PyString_FromString(const char *str)
{
register size_t size;
register PyStringObject *op;

……//创建PyStringObject对象

// intern(共享)长度较短的PyStringObject对象
if (size == 0) {
// 如果是空字符串, 则会初始化nullstring, 并缓存
PyObject *t = (PyObject *)op;
PyString_InternInPlace(&t);
op = (PyStringObject *)t;
nullstring = op;
} else if (size == 1) {
// 如果字符长度为1,则会进入intern机制
PyObject *t = (PyObject *)op;
PyString_InternInPlace(&t);
op = (PyStringObject *)t;
characters[*str & UCHAR_MAX] = op;
}
return (PyObject *) op;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
void PyString_InternInPlace(PyObject **p)
{
register PyStringObject *s = (PyStringObject *)(*p);
PyObject *t;
if (s == NULL || !PyString_Check(s))
Py_FatalError("PyString_InternInPlace: strings only please!");
/* If it's a string subclass, we don't really know what putting
it in the interned dict might do. */
if (!PyString_CheckExact(s))
return;
if (PyString_CHECK_INTERNED(s))
return;
if (interned == NULL) {
interned = PyDict_New();
if (interned == NULL) {
PyErr_Clear(); /* Don't leave an exception */
return;
}
}
t = PyDict_GetItem(interned, (PyObject *)s);
// 如果已经在interned字典里面, 则直接返回字典里面的对象, 并减少新创建对象的引用计数(实际上新创建的对象引用计数变为0了)
if (t) {
Py_INCREF(t);
Py_DECREF(*p);
*p = t;
return;
}
// 如果还未缓存, 则缓存在字典中
if (PyDict_SetItem(interned, (PyObject *)s, (PyObject *)s) < 0) {
PyErr_Clear();
return;
}
/* The two references in interned are not counted by refcnt.
The string deallocator will take care of this */
s->ob_refcnt -= 2;
PyString_CHECK_INTERNED(s) = SSTATE_INTERNED_MORTAL;
}

理解上面的代码可以看出:

  • PyString_InternInPlace 实际上是执行intern机制, 只有string类型对象才允许用于intern机制(父类为string类型的对象也不行)

  • PyString_FromString 首先会创建一个字符串对象出来, 如果字符串对象已被缓存, 则新创建的字符串实际上被丢弃了,等待GC去回收/复用这部分内存

上面的代码,只看到对于字符串长度为0或者1个,会进入intern机制, 但是实际上肯定不止这些字符串能被缓存, 书上没讲判别是否进入intern机制的条件是什么,查了相关资料, 发现这可能还跟编译器的优化相关, 这里就暂时不去深究了

interning 字符串长度大于1的对象的代码在下面

1
2
3
4
5
6
7
8
9
[stringobject.c]
PyObject* PyString_InternFromString(const char *cp)
{
// 长度大于1的字符串对象不会在这里被interning
PyObject *s = PyString_FromString(cp);
// 在这里进行interning
PyString_InternInPlace(&s);
return s;
}
1
2
3
4
5
6
7
/*
Interning strings (ob_sstate) tries to ensure that only one string
object with a given value exists, so equality tests can be one pointer
comparison. This is generally restricted to strings that "look like"
Python identifiers, although the intern() builtin can be used to force
interning of any string.
*/

上面这段官方的解释也比较抽象, intern可以被用于interning任何string, This is generally restricted to strings that "look like" Python identifiers 这句话就不是很理解了

总结

简单来讲

  • 对于字符串长度为0和1的对象, Python会缓存(interning)在一个数组长度为256的字符串对象数组里面, 这个数组里面的对象并非在Python运行前就创建并初始化好, 而是在创建指定对象后缓存在数组里面的
1
2
#define	UCHAR_MAX	255		/* max value for an unsigned char */
static PyStringObject *characters[UCHAR_MAX + 1]; // 用于存储单字符对象
  • 对于字符串长度大于1的对象, Python会有条件的缓存这个对象(具体条件未知…)

本文标题:Python源码剖析 第三章 字符串对象

文章作者:定。

发布时间:2019年4月27日 - 23时04分

本文字数:7,088字

原始链接:http://cocofe.cn/2019/04/27/python源码剖析-第三章-字符串对象/

许可协议: Attribution-NonCommercial 4.0

转载请保留以上信息。