diff --git "a/src/24.bytes\345\257\271\350\261\241\351\203\275\346\224\257\346\214\201\345\223\252\344\272\233\346\223\215\344\275\234\357\274\214\345\256\203\344\273\254\346\230\257\346\200\216\344\271\210\345\256\236\347\216\260\347\232\204\357\274\237 .md" "b/src/24.bytes\345\257\271\350\261\241\351\203\275\346\224\257\346\214\201\345\223\252\344\272\233\346\223\215\344\275\234\357\274\214\345\256\203\344\273\254\346\230\257\346\200\216\344\271\210\345\256\236\347\216\260\347\232\204\357\274\237.md" similarity index 100% rename from "src/24.bytes\345\257\271\350\261\241\351\203\275\346\224\257\346\214\201\345\223\252\344\272\233\346\223\215\344\275\234\357\274\214\345\256\203\344\273\254\346\230\257\346\200\216\344\271\210\345\256\236\347\216\260\347\232\204\357\274\237 .md" rename to "src/24.bytes\345\257\271\350\261\241\351\203\275\346\224\257\346\214\201\345\223\252\344\272\233\346\223\215\344\275\234\357\274\214\345\256\203\344\273\254\346\230\257\346\200\216\344\271\210\345\256\236\347\216\260\347\232\204\357\274\237.md" diff --git "a/src/26.\350\247\243\345\257\206bytes\345\257\271\350\261\241\347\232\204\347\274\223\345\255\230\346\261\240.md" "b/src/26.\350\247\243\345\257\206bytes\345\257\271\350\261\241\347\232\204\347\274\223\345\255\230\346\261\240.md" new file mode 100644 index 0000000..23c9bb3 --- /dev/null +++ "b/src/26.\350\247\243\345\257\206bytes\345\257\271\350\261\241\347\232\204\347\274\223\345\255\230\346\261\240.md" @@ -0,0 +1,117 @@ +bytes 对象是不可变对象,那么根据我们对浮点数的了解,可以大胆猜测 bytes 对象也有自己的缓存池。事实上确实如此,为了优化单字节 bytes 对象的创建效率,Python 底层维护了一个缓存池,该缓存池是一个 PyBytesObject \* 类型的数组。 + +~~~C +// Objects/bytesobject.c + +// 保存了 256 个单字节 bytes 对象,对应的 ASCII 码为 0 ~ 255 +static PyBytesObject *characters[UCHAR_MAX + 1]; +// 保存空 bytes 对象 +static PyBytesObject *nullstring; +~~~ + +Python 内部创建单字节 bytes 对象时,会先检查目标对象是否已在缓存池中。PyBytes_FromString 函数是负责创建 bytes 对象的一个常用的 Python/C API,我们看一下它的逻辑。 + +~~~C +// Objects/bytesobject.c + +// 基于 C 字符串创建 bytes 对象 +PyObject * +PyBytes_FromString(const char *str) +{ + size_t size; // bytes 对象的长度 + PyBytesObject *op; // 指向创建的 bytes 对象 + + assert(str != NULL); + // 计算 C 字符串的长度,它和对应的 bytes 对象的长度是相等的 + size = strlen(str); + if (size > PY_SSIZE_T_MAX - PyBytesObject_SIZE) { + PyErr_SetString(PyExc_OverflowError, + "byte string is too long"); + return NULL; + } + + // 如果 size 等于 0,并且 nullstring 保存了空 bytes 对象,那么直接返回 + if (size == 0 && (op = nullstring) != NULL) { +#ifdef COUNT_ALLOCS + _Py_null_strings++; +#endif + Py_INCREF(op); + return (PyObject *)op; + } + + // 如果 size 等于 1,比如 char *str = "a",证明创建的是单字节对象 + // 而 str 是字符串首元素的地址,所以 *str 会得到 'a',即 97 + // 假设 *str 是 97,那么 op 就是 bytes_characters[97] + if (size == 1 && (op = characters[*str & UCHAR_MAX]) != NULL) { +#ifdef COUNT_ALLOCS + _Py_one_strings++; +#endif + Py_INCREF(op); + return (PyObject *)op; + } + + // 否则创建新的 PyBytesObject 对象,此时是个空 + op = (PyBytesObject *)PyObject_MALLOC(PyBytesObject_SIZE + size); + if (op == NULL) + return PyErr_NoMemory(); + // 初始化内部字段 + (void)PyObject_INIT_VAR(op, &PyBytes_Type, size); + op->ob_shash = -1; + // 将 C 字符串拷贝到 ob_sval 中 + memcpy(op->ob_sval, str, size+1); + // 如果 size == 0,说明创建的是空 bytes 对象,那么赋值给 nullstring + if (size == 0) { + nullstring = op; + Py_INCREF(op); + } else if (size == 1) { + // 如果 size == 1,说明创建的是单字节 bytes 对象,那么放入到缓存池中 + // 并且在缓存池中的索引,就是该字节对应的 ASCII 码 + characters[*str & UCHAR_MAX] = op; + Py_INCREF(op); + } + // 转成泛型指针之后返回 + return (PyObject *) op; +} +~~~ + +整体来说并不难,该 API 会将 C 字符串全部拷贝到 ob_sval 中。但如果 C 字符串长度为 10,而我们只希望基于前 n 个字符创建 bytes 对象,这时候可以使用 PyBytes_FromStringAndSize 函数。 + ++ PyBytes_FromString("Hello World") 会返回 b"Hello World"。 ++ PyBytes_FromStringAndSize("Hello World", 5) 会返回 b"Hello",如果传递的第二个参数 size 和 C 字符串长度相同,那么效果和 PyBytes_FromString 是等价的。 + +至于 PyBytes_FromStringAndSize 的逻辑和 PyBytes_FromString 类似,缓存池部分也是一样的,可以自己看一下。 + +当 Python 程序开始运行时,字节序列缓存池是空的。但随着单字节 bytes 对象的创建,缓存池中的对象慢慢多了起来。这样一来,单字节序列首次创建后便在缓存池中缓存起来,后续再使用时会直接从缓存池中获取,避免重复创建和销毁。与前面章节介绍的小整数对象池一样,字节序列缓存池也只能容纳为数不多的 256 个单字节序列,但使用频率非常高。 + +![](./images/85.png) + +缓存池技术作为一种以空间换时间的优化手段,只需较小的内存为代价,便可明显提升执行效率。 + +~~~python +>>> a1 = b"a" +>>> a2 = b"a" +>>> a1 is a2 +True +>>> +>>> a1 = b"ab" +>>> a2 = b"ab" +>>> a1 is a2 +False +>>> +~~~ + +显然此时不需要解释了,单字节 bytes 对象会缓存起来,放到缓存池中。至于空字节 bytes 对象,则是由专门的 nullstring 变量保存,它们都是单例的。 + +到目前为止,关于 bytes 对象的内容就说完了。 + +----- + +  + +**欢迎大家关注我的公众号:古明地觉的编程教室。** + +![](./images/qrcode_for_gh.jpg) + +**如果觉得文章对你有所帮助,也可以请作者吃个馒头,Thanks♪(・ω・)ノ。** + +![](./images/supports.png) \ No newline at end of file diff --git a/src/SUMMARY.md b/src/SUMMARY.md index f5d4d72..e8a9c69 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -24,5 +24,6 @@ + [21. Python 的 None 是怎么实现的?](21.Python的None是怎么实现的?.md) + [22.深度解密 Python 切片的实现原理](22.深度解密Python切片的实现原理.md) + [23. bytes 对象(字节串)是怎么实现的?解密它的内部原理](23.bytes对象(字节串)是怎么实现的?解密它的内部原理.md) -+ [24. bytes 对象都支持哪些操作,它们是怎么实现的? ](24.bytes对象都支持哪些操作,它们是怎么实现的? .md) -+ [25. 通过 bytes 对象的合并,探究缓冲区的奥秘](25.通过bytes对象的合并,探究缓冲区的奥秘.md) \ No newline at end of file ++ [24. bytes 对象都支持哪些操作,它们是怎么实现的?](24.bytes对象都支持哪些操作,它们是怎么实现的?.md) ++ [25. 通过 bytes 对象的合并,探究缓冲区的奥秘](25.通过bytes对象的合并,探究缓冲区的奥秘.md) ++ [26. 解密 bytes 对象的缓存池](26.解密bytes对象的缓存池.md) \ No newline at end of file diff --git a/src/images/85.png b/src/images/85.png new file mode 100644 index 0000000..6be76d6 Binary files /dev/null and b/src/images/85.png differ