经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » 程序设计 » Python » 查看文章
深入理解 Python 虚拟机:字节(bytes)的实现原理及源码剖析
来源:cnblogs  作者:一无是处的研究僧  时间:2023/3/24 9:05:20  对本文有异议

深入理解 Python 虚拟机:字节(bytes)的实现原理及源码剖析

在本篇文章当中主要给大家介绍在 cpython 内部,bytes 的实现原理、内存布局以及与 bytes 相关的一个比较重要的优化点—— bytes 的拼接。

数据结构

  1. typedef struct {
  2. PyObject_VAR_HEAD
  3. Py_hash_t ob_shash;
  4. char ob_sval[1];
  5. /* Invariants:
  6. * ob_sval contains space for 'ob_size+1' elements.
  7. * ob_sval[ob_size] == 0.
  8. * ob_shash is the hash of the string or -1 if not computed yet.
  9. */
  10. } PyBytesObject;
  11. typedef struct {
  12. PyObject ob_base;
  13. Py_ssize_t ob_size; /* Number of items in variable part */
  14. } PyVarObject;
  15. typedef struct _object {
  16. Py_ssize_t ob_refcnt;
  17. struct _typeobject *ob_type;
  18. } PyObject;

上面的数据结构用图示如下所示:

现在我们来解释一下上面的数据结构各个字段的含义:

  • ob_refcnt,这个还是对象的引用计数的个数,主要是在垃圾回收的时候有用。
  • ob_type,这个是对象的数据类型。
  • ob_size,表示这个对象当中字节的个数。
  • ob_shash,对象的哈希值,如果还没有计算,哈希值为 -1 。
  • ob_sval,一个数据存储一个字节的数据,需要注意的是 ob_sval[size] 一定等于 '\0' ,表示字符串的结尾。

可能你会有疑问上面的结构体当中并没有后面的那么多字节啊,数组只有一个字节的数据啊,这是因为在 cpython 的实现当中除了申请 PyBytesObject 大的小内存空间之外,还会在这个基础之上申请连续的额外的内存空间用于保存数据,在后续的源码分析当中可以看到这一点。

下面我们举几个例子来说明一下上面的布局:

上面是空和字符串 abc 的字节表示。

创建字节对象

下面是在 cpython 当中通过字节数创建 PyBytesObject 对象的函数。下面的函数的主要功能是创建一个能够存储 size 个字节大小的数据的 PyBytesObject 对象,下面的函数最重要的一个步骤就是申请内存空间。

  1. static PyObject *
  2. _PyBytes_FromSize(Py_ssize_t size, int use_calloc)
  3. {
  4. PyBytesObject *op;
  5. assert(size >= 0);
  6. if (size == 0 && (op = nullstring) != NULL) {
  7. #ifdef COUNT_ALLOCS
  8. null_strings++;
  9. #endif
  10. Py_INCREF(op);
  11. return (PyObject *)op;
  12. }
  13. if ((size_t)size > (size_t)PY_SSIZE_T_MAX - PyBytesObject_SIZE) {
  14. PyErr_SetString(PyExc_OverflowError,
  15. "byte string is too large");
  16. return NULL;
  17. }
  18. /* Inline PyObject_NewVar */
  19. // PyBytesObject_SIZE + size 就是实际申请的内存空间的大小 PyBytesObject_SIZE 就是表示 PyBytesObject 各个字段占用的实际的内存空间大小
  20. if (use_calloc)
  21. op = (PyBytesObject *)PyObject_Calloc(1, PyBytesObject_SIZE + size);
  22. else
  23. op = (PyBytesObject *)PyObject_Malloc(PyBytesObject_SIZE + size);
  24. if (op == NULL)
  25. return PyErr_NoMemory();
  26. // 将对象的 ob_size 字段赋值成 size
  27. (void)PyObject_INIT_VAR(op, &PyBytes_Type, size);
  28. // 由于对象的哈希值还没有进行计算 因此现将哈希值赋值成 -1
  29. op->ob_shash = -1;
  30. if (!use_calloc)
  31. op->ob_sval[size] = '\0';
  32. /* empty byte string singleton */
  33. if (size == 0) {
  34. nullstring = op;
  35. Py_INCREF(op);
  36. }
  37. return (PyObject *) op;
  38. }

我们可以使用一个写例子来看一下实际的 PyBytesObject 内存空间的大小。

  1. >>> import sys
  2. >>> a = b"hello world"
  3. >>> sys.getsizeof(a)
  4. 44
  5. >>>

上面的 44 = 32 + 11 + 1 。

其中 32 是 PyBytesObject 4 个字段所占用的内存空间,ob_refcnt、ob_type、ob_size和 ob_shash 各占 8 个字节。11 是表示字符串 "hello world" 占用 11 个字节,最后一个字节是 '\0' 。

查看字节长度

这个函数主要是返回 PyBytesObject 对象的字节长度,也就是直接返回 ob_size 的值。

  1. static Py_ssize_t
  2. bytes_length(PyBytesObject *a)
  3. {
  4. // (((PyVarObject*)(ob))->ob_size)
  5. return Py_SIZE(a);
  6. }

字节拼接

在 python 当中执行下面的代码就会执行字节拼接函数:

  1. >>> b"abc" + b"edf"

下方就是具体的执行字节拼接的函数:

  1. /* This is also used by PyBytes_Concat() */
  2. static PyObject *
  3. bytes_concat(PyObject *a, PyObject *b)
  4. {
  5. Py_buffer va, vb;
  6. PyObject *result = NULL;
  7. va.len = -1;
  8. vb.len = -1;
  9. // Py_buffer 当中有一个指针字段 buf 可以用户保存 PyBytesObject 当中字节数据的首地址
  10. // PyObject_GetBuffer 函数的主要作用是将 对象 a 当中的字节数组赋值给 va 当中的 buf
  11. if (PyObject_GetBuffer(a, &va, PyBUF_SIMPLE) != 0 ||
  12. PyObject_GetBuffer(b, &vb, PyBUF_SIMPLE) != 0) {
  13. PyErr_Format(PyExc_TypeError, "can't concat %.100s to %.100s",
  14. Py_TYPE(b)->tp_name, Py_TYPE(a)->tp_name);
  15. goto done;
  16. }
  17. /* Optimize end cases */
  18. if (va.len == 0 && PyBytes_CheckExact(b)) {
  19. result = b;
  20. Py_INCREF(result);
  21. goto done;
  22. }
  23. if (vb.len == 0 && PyBytes_CheckExact(a)) {
  24. result = a;
  25. Py_INCREF(result);
  26. goto done;
  27. }
  28. if (va.len > PY_SSIZE_T_MAX - vb.len) {
  29. PyErr_NoMemory();
  30. goto done;
  31. }
  32. result = PyBytes_FromStringAndSize(NULL, va.len + vb.len);
  33. // 下方就是将对象 a b 当中的字节数据拷贝到新的
  34. if (result != NULL) {
  35. // PyBytes_AS_STRING 宏定义在下方当中 主要就是使用 PyBytesObject 对象当中的
  36. // ob_sval 字段 也就是将 buf 数据(也就是 a 或者 b 当中的字节数据)拷贝到 ob_sval当中
  37. memcpy(PyBytes_AS_STRING(result), va.buf, va.len);
  38. memcpy(PyBytes_AS_STRING(result) + va.len, vb.buf, vb.len);
  39. }
  40. done:
  41. if (va.len != -1)
  42. PyBuffer_Release(&va);
  43. if (vb.len != -1)
  44. PyBuffer_Release(&vb);
  45. return result;
  46. }
  1. #define PyBytes_AS_STRING(op) (assert(PyBytes_Check(op)), (((PyBytesObject *)(op))->ob_sval))

我们修改一个这个函数,在其中加入一条打印语句,然后重新编译 python 执行结果如下所示:

  1. Python 3.9.0b1 (default, Mar 23 2023, 08:35:33)
  2. [GCC 4.8.5 20150623 (Red Hat 4.8.5-44)] on linux
  3. Type "help", "copyright", "credits" or "license" for more information.
  4. >>> b"abc" + b"edf"
  5. In concat function: abc <> edf
  6. b'abcedf'
  7. >>>

在上面的拼接函数当中会拷贝原来的两个字节对象,因此需要谨慎使用,一旦发生非常多的拷贝的话是非常耗费内存的。因此需要警惕使用循环内的内存拼接。比如对于 [b"a", b"b", b"c"] 来说,如果使用循环拼接的话,那么会将 b"a" 拷贝两次。

  1. >>> res = b""
  2. >>> for item in [b"a", b"b", b"c"]:
  3. ... res += item
  4. ...
  5. >>> res
  6. b'abc'
  7. >>>

因为 b"a", b"b" 在拼接的时候会将他们分别拷贝一次,在进行 b"ab",b"c" 拼接的时候又会将 ab 和 c 拷贝一次,那么具体的拷贝情况如下所示:

  • "a" 拷贝了一次。
  • "b" 拷贝了一次。
  • "ab" 拷贝了一次。
  • "c" 拷贝了一次。

但是实际上我们的需求是只需要对 [b"a", b"b", b"c"] 当中的数据各拷贝一次,如果我们要实现这一点可以使用 b"".join([b"a", b"b", b"c"]),直接将 [b"a", b"b", b"c"] 作为参数传递,然后各自只拷贝一次,具体的实现代码如下所示,在这个例子当中 sep 就是空串 b"",iterable 就是 [b"a", b"b", b"c"] 。

  1. Py_LOCAL_INLINE(PyObject *)
  2. STRINGLIB(bytes_join)(PyObject *sep, PyObject *iterable)
  3. {
  4. char *sepstr = STRINGLIB_STR(sep);
  5. const Py_ssize_t seplen = STRINGLIB_LEN(sep);
  6. PyObject *res = NULL;
  7. char *p;
  8. Py_ssize_t seqlen = 0;
  9. Py_ssize_t sz = 0;
  10. Py_ssize_t i, nbufs;
  11. PyObject *seq, *item;
  12. Py_buffer *buffers = NULL;
  13. #define NB_STATIC_BUFFERS 10
  14. Py_buffer static_buffers[NB_STATIC_BUFFERS];
  15. seq = PySequence_Fast(iterable, "can only join an iterable");
  16. if (seq == NULL) {
  17. return NULL;
  18. }
  19. seqlen = PySequence_Fast_GET_SIZE(seq);
  20. if (seqlen == 0) {
  21. Py_DECREF(seq);
  22. return STRINGLIB_NEW(NULL, 0);
  23. }
  24. #ifndef STRINGLIB_MUTABLE
  25. if (seqlen == 1) {
  26. item = PySequence_Fast_GET_ITEM(seq, 0);
  27. if (STRINGLIB_CHECK_EXACT(item)) {
  28. Py_INCREF(item);
  29. Py_DECREF(seq);
  30. return item;
  31. }
  32. }
  33. #endif
  34. if (seqlen > NB_STATIC_BUFFERS) {
  35. buffers = PyMem_NEW(Py_buffer, seqlen);
  36. if (buffers == NULL) {
  37. Py_DECREF(seq);
  38. PyErr_NoMemory();
  39. return NULL;
  40. }
  41. }
  42. else {
  43. buffers = static_buffers;
  44. }
  45. /* Here is the general case. Do a pre-pass to figure out the total
  46. * amount of space we'll need (sz), and see whether all arguments are
  47. * bytes-like.
  48. */
  49. for (i = 0, nbufs = 0; i < seqlen; i++) {
  50. Py_ssize_t itemlen;
  51. item = PySequence_Fast_GET_ITEM(seq, i);
  52. if (PyBytes_CheckExact(item)) {
  53. /* Fast path. */
  54. Py_INCREF(item);
  55. buffers[i].obj = item;
  56. buffers[i].buf = PyBytes_AS_STRING(item);
  57. buffers[i].len = PyBytes_GET_SIZE(item);
  58. }
  59. else if (PyObject_GetBuffer(item, &buffers[i], PyBUF_SIMPLE) != 0) {
  60. PyErr_Format(PyExc_TypeError,
  61. "sequence item %zd: expected a bytes-like object, "
  62. "%.80s found",
  63. i, Py_TYPE(item)->tp_name);
  64. goto error;
  65. }
  66. nbufs = i + 1; /* for error cleanup */
  67. itemlen = buffers[i].len;
  68. if (itemlen > PY_SSIZE_T_MAX - sz) {
  69. PyErr_SetString(PyExc_OverflowError,
  70. "join() result is too long");
  71. goto error;
  72. }
  73. sz += itemlen;
  74. if (i != 0) {
  75. if (seplen > PY_SSIZE_T_MAX - sz) {
  76. PyErr_SetString(PyExc_OverflowError,
  77. "join() result is too long");
  78. goto error;
  79. }
  80. sz += seplen;
  81. }
  82. if (seqlen != PySequence_Fast_GET_SIZE(seq)) {
  83. PyErr_SetString(PyExc_RuntimeError,
  84. "sequence changed size during iteration");
  85. goto error;
  86. }
  87. }
  88. /* Allocate result space. */
  89. res = STRINGLIB_NEW(NULL, sz);
  90. if (res == NULL)
  91. goto error;
  92. /* Catenate everything. */
  93. p = STRINGLIB_STR(res);
  94. if (!seplen) {
  95. /* fast path */
  96. for (i = 0; i < nbufs; i++) {
  97. Py_ssize_t n = buffers[i].len;
  98. char *q = buffers[i].buf;
  99. Py_MEMCPY(p, q, n);
  100. p += n;
  101. }
  102. goto done;
  103. }
  104. // 具体的实现逻辑就是在这里
  105. for (i = 0; i < nbufs; i++) {
  106. Py_ssize_t n;
  107. char *q;
  108. if (i) {
  109. // 首先现将 sepstr 拷贝到新的数组里面但是在我们举的例子当中是空串 b""
  110. Py_MEMCPY(p, sepstr, seplen);
  111. p += seplen;
  112. }
  113. n = buffers[i].len;
  114. q = buffers[i].buf;
  115. // 然后将列表当中第 i 个 bytes 的数据拷贝到 p 当中 这样就是实现了我们所需要的效果
  116. Py_MEMCPY(p, q, n);
  117. p += n;
  118. }
  119. goto done;
  120. error:
  121. res = NULL;
  122. done:
  123. Py_DECREF(seq);
  124. for (i = 0; i < nbufs; i++)
  125. PyBuffer_Release(&buffers[i]);
  126. if (buffers != static_buffers)
  127. PyMem_FREE(buffers);
  128. return res;
  129. }

单字节字符

在 cpython 的内部实现当中给单字节的字符做了一个小的缓冲池:

  1. static PyBytesObject *characters[UCHAR_MAX + 1]; // UCHAR_MAX 在 64 位系统当中等于 255

当创建的 bytes 只有一个字符的时候就可以检查是否 characters 当中已经存在了,如果存在就直接返回这个已经创建好的 PyBytesObject 对象,否则再进行创建。新创建的 PyBytesObject 对象如果长度等于 1 的话也会被加入到这个数组当中。下面是 PyBytesObject 的另外一个创建函数:

  1. PyObject *
  2. PyBytes_FromStringAndSize(const char *str, Py_ssize_t size)
  3. {
  4. PyBytesObject *op;
  5. if (size < 0) {
  6. PyErr_SetString(PyExc_SystemError,
  7. "Negative size passed to PyBytes_FromStringAndSize");
  8. return NULL;
  9. }
  10. // 如果创建长度等于 1 而且对象在 characters 当中存在的话那么就直接返回
  11. if (size == 1 && str != NULL &&
  12. (op = characters[*str & UCHAR_MAX]) != NULL)
  13. {
  14. #ifdef COUNT_ALLOCS
  15. one_strings++;
  16. #endif
  17. Py_INCREF(op);
  18. return (PyObject *)op;
  19. }
  20. op = (PyBytesObject *)_PyBytes_FromSize(size, 0);
  21. if (op == NULL)
  22. return NULL;
  23. if (str == NULL)
  24. return (PyObject *) op;
  25. Py_MEMCPY(op->ob_sval, str, size);
  26. /* share short strings */
  27. // 如果创建的对象的长度等于 1 那么久将这个对象保存到 characters 当中
  28. if (size == 1) {
  29. characters[*str & UCHAR_MAX] = op;
  30. Py_INCREF(op);
  31. }
  32. return (PyObject *) op;
  33. }

我们可以使用下面的代码进行验证:

  1. >>> a = b"a"
  2. >>> b =b"a"
  3. >>> a == b
  4. True
  5. >>> a is b
  6. True
  7. >>> a = b"aa"
  8. >>> b = b"aa"
  9. >>> a == b
  10. True
  11. >>> a is b
  12. False

从上面的代码可以知道,确实当我们创建的 bytes 的长度等于 1 的时候对象确实是同一个对象。

总结

在本篇文章当中主要给大家介绍了在 cpython 内部对于 bytes 的实现,重点介绍了 cpython 当中 PyBytesObject 的内存布局和创建 PyBytesObject 的函数,以及对于 bytes 对象的拼接细节和 cpython 内部单字节字符的缓冲池。在程序当中最好使用 join 操作进行 btyes 的拼接操作,否则效率会比较低。


本篇文章是深入理解 python 虚拟机系列文章之一,文章地址:https://github.com/Chang-LeHung/dive-into-cpython

更多精彩内容合集可访问项目:https://github.com/Chang-LeHung/CSCore

关注公众号:一无是处的研究僧,了解更多计算机(Java、Python、计算机系统基础、算法与数据结构)知识。

原文链接:https://www.cnblogs.com/Chang-LeHung/p/17250059.html

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号